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
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context, Cancel

async ValueTask<List<ChatMessage>?> InvokeTakeTurnAsync(List<ChatMessage>? maybePendingMessages, IWorkflowContext context, CancellationToken cancellationToken)
{
await this.TakeTurnAsync(maybePendingMessages ?? s_initFunction(), context, token.EmitEvents, cancellationToken)
await this.TakeTurnAsync(maybePendingMessages ?? s_initFunction(), context, token, cancellationToken)
.ConfigureAwait(false);

if (this.AutoSendTurnToken)
Expand All @@ -154,6 +154,17 @@ await this.TakeTurnAsync(maybePendingMessages ?? s_initFunction(), context, toke
}
}

/// <summary>
/// When overridden in a derived class, processes the accumulated chat messages and the full turn token.
/// </summary>
/// <param name="messages">The list of chat messages accumulated since the last turn.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="turnToken">The token containing turn configuration, including any agent run options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
protected virtual ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, TurnToken turnToken, CancellationToken cancellationToken = default)
=> this.TakeTurnAsync(messages, context, turnToken.EmitEvents, cancellationToken);

/// <summary>
/// Processes the current set of turn messages using the specified asynchronous processing function.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,8 @@ ValueTask<T> ReadOrInitStateAsync<T>(string key, Func<T> initialStateFactory, Ca
/// </summary>
bool ConcurrentRunsEnabled { get; }
}

internal interface IWorkflowAgentRunOptionsContext
{
bool TryGetAgentRunOptions(out AgentRunOptions? runOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ namespace Microsoft.Agents.AI.Workflows;
/// </summary>
public static class IWorkflowContextExtensions
{
internal static AgentRunOptions? GetAgentRunOptions(this IWorkflowContext context, AgentRunOptions? fallback)
{
return context is IWorkflowAgentRunOptionsContext invocationContext
&& invocationContext.TryGetAgentRunOptions(out AgentRunOptions? runOptions)
? runOptions
: fallback;
}

/// <summary>
/// Invokes an asynchronous operation that reads, updates, and persists workflow state associated with the specified
/// key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ public async ValueTask<StreamingRun> RunStreamingAsync<TInput>(
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
}

internal async ValueTask<StreamingRun> RunStreamingWithAgentRunOptionsAsync<TInput>(
Workflow workflow,
TInput input,
AgentRunOptions? runOptions,
string? sessionId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(
workflow,
this.CheckpointManager,
sessionId,
this.EnableConcurrentRuns,
knownValidInputTypes: [],
hasAgentRunOptions: true,
runOptions);
AsyncRunHandle runHandle = await runner.BeginStreamAsync(this.ExecutionMode, cancellationToken).ConfigureAwait(false);

return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
}

[MemberNotNull(nameof(CheckpointManager))]
private void VerifyCheckpointingConfigured()
{
Expand Down Expand Up @@ -133,6 +153,29 @@ internal async ValueTask<StreamingRun> ResumeStreamingInternalAsync(
return new(runHandle);
}

internal async ValueTask<StreamingRun> ResumeStreamingWithAgentRunOptionsInternalAsync(
Workflow workflow,
CheckpointInfo fromCheckpoint,
bool republishPendingEvents,
AgentRunOptions? runOptions,
CancellationToken cancellationToken = default)
{
this.VerifyCheckpointingConfigured();

InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(
workflow,
this.CheckpointManager,
fromCheckpoint.SessionId,
this.EnableConcurrentRuns,
knownValidInputTypes: [],
hasAgentRunOptions: true,
runOptions);
AsyncRunHandle runHandle = await runner.ResumeStreamAsync(this.ExecutionMode, fromCheckpoint, republishPendingEvents, cancellationToken)
.ConfigureAwait(false);

return new(runHandle);
}

private async ValueTask<AsyncRunHandle> BeginRunHandlingChatProtocolAsync<TInput>(Workflow workflow,
TInput input,
string? sessionId = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,31 @@ namespace Microsoft.Agents.AI.Workflows.InProc;
/// scenarios where workflow execution does not require executor distribution. </para></remarks>
internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
{
public static InProcessRunner CreateTopLevelRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null)
public static InProcessRunner CreateTopLevelRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null, bool hasAgentRunOptions = false, AgentRunOptions? agentRunOptions = null)
{
return new InProcessRunner(workflow,
checkpointManager,
sessionId,
enableConcurrentRuns: enableConcurrentRuns,
knownValidInputTypes: knownValidInputTypes);
knownValidInputTypes: knownValidInputTypes,
hasAgentRunOptions: hasAgentRunOptions,
agentRunOptions: agentRunOptions);
}

public static InProcessRunner CreateSubworkflowRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, object? existingOwnerSignoff = null, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null)
public static InProcessRunner CreateSubworkflowRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, object? existingOwnerSignoff = null, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null, bool hasAgentRunOptions = false, AgentRunOptions? agentRunOptions = null)
{
return new InProcessRunner(workflow,
checkpointManager,
sessionId,
existingOwnerSignoff: existingOwnerSignoff,
enableConcurrentRuns: enableConcurrentRuns,
knownValidInputTypes: knownValidInputTypes,
hasAgentRunOptions: hasAgentRunOptions,
agentRunOptions: agentRunOptions,
subworkflow: true);
}

private InProcessRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, object? existingOwnerSignoff = null, bool subworkflow = false, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null)
private InProcessRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? sessionId = null, object? existingOwnerSignoff = null, bool subworkflow = false, bool enableConcurrentRuns = false, IEnumerable<Type>? knownValidInputTypes = null, bool hasAgentRunOptions = false, AgentRunOptions? agentRunOptions = null)
{
if (enableConcurrentRuns && !workflow.AllowConcurrent)
{
Expand All @@ -54,7 +58,7 @@ private InProcessRunner(Workflow workflow, ICheckpointManager? checkpointManager
this.StartExecutorId = workflow.StartExecutorId;

this.Workflow = Throw.IfNull(workflow);
this.RunContext = new InProcessRunnerContext(workflow, this.SessionId, checkpointingEnabled: checkpointManager != null, this.OutgoingEvents, this.StepTracer, existingOwnerSignoff, subworkflow, enableConcurrentRuns);
this.RunContext = new InProcessRunnerContext(workflow, this.SessionId, checkpointingEnabled: checkpointManager != null, this.OutgoingEvents, this.StepTracer, existingOwnerSignoff, subworkflow, enableConcurrentRuns, hasAgentRunOptions, agentRunOptions);
this.CheckpointManager = checkpointManager;

this._knownValidInputTypes = knownValidInputTypes != null
Expand Down Expand Up @@ -259,6 +263,15 @@ await executor.OnMessageDeliveryStartingAsync(tracelessContext, cancellationToke
{
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);

// A restored token can contain options from an earlier in-memory run or no options after
// serialization. The options supplied for the current invocation take precedence.
if (message is TurnToken turnToken
&& this.RunContext.TryGetAgentRunOptions(out AgentRunOptions? runOptions)
&& !ReferenceEquals(turnToken.RunOptions, runOptions))
{
message = new TurnToken(turnToken.EmitEvents, runOptions);
}

await executor.ExecuteCoreAsync(
message,
messageType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

namespace Microsoft.Agents.AI.Workflows.InProc;

internal sealed class InProcessRunnerContext : IRunnerContext
internal sealed class InProcessRunnerContext : IRunnerContext, IWorkflowAgentRunOptionsContext
{
private int _runEnded;
private readonly string _sessionId;
Expand All @@ -37,6 +37,8 @@ internal sealed class InProcessRunnerContext : IRunnerContext
private readonly ConcurrentDictionary<string, ISuperStepRunner> _joinedSubworkflowRunners = new();

private readonly ConcurrentDictionary<string, ExternalRequest> _externalRequests = new();
private readonly bool _hasAgentRunOptions;
private readonly AgentRunOptions? _agentRunOptions;

public InProcessRunnerContext(
Workflow workflow,
Expand All @@ -47,6 +49,8 @@ public InProcessRunnerContext(
object? existingOwnershipSignoff = null,
bool subworkflow = false,
bool enableConcurrentRuns = false,
bool hasAgentRunOptions = false,
AgentRunOptions? agentRunOptions = null,
ILogger? logger = null)
{
if (enableConcurrentRuns)
Expand All @@ -62,6 +66,8 @@ public InProcessRunnerContext(

this._workflow = workflow;
this._sessionId = sessionId;
this._hasAgentRunOptions = hasAgentRunOptions;
this._agentRunOptions = agentRunOptions;

this._edgeMap = new(this, this._workflow, stepTracer);
this._outputFilter = new(workflow);
Expand Down Expand Up @@ -310,6 +316,12 @@ public IWorkflowContext BindWorkflowContext(string executorId, Dictionary<string
return new BoundWorkflowContext(this, executorId, traceContext);
}

public bool TryGetAgentRunOptions(out AgentRunOptions? runOptions)
{
runOptions = this._agentRunOptions;
return this._hasAgentRunOptions;
}

public ValueTask PostAsync(ExternalRequest request)
{
this.CheckEnded();
Expand Down Expand Up @@ -347,8 +359,13 @@ public IExternalRequestSink RegisterPort(RequestPort port)
private sealed class BoundWorkflowContext(
InProcessRunnerContext RunnerContext,
string ExecutorId,
Dictionary<string, string>? traceContext) : IWorkflowContext
Dictionary<string, string>? traceContext) : IWorkflowContext, IWorkflowAgentRunOptionsContext
{
public bool TryGetAgentRunOptions(out AgentRunOptions? runOptions)
{
return RunnerContext.TryGetAgentRunOptions(out runOptions);
}

public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default) => RunnerContext.AddEventAsync(workflowEvent, cancellationToken);

public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class AIAgentHostExecutor : ChatProtocolExecutor
private readonly AIAgentHostOptions _options;
private AgentSession? _session;
private bool? _currentTurnEmitEvents;
private AgentRunOptions? _currentTurnRunOptions;

private AIContentExternalHandler<ToolApprovalRequestContent, ToolApprovalResponseContent>? _userInputHandler;
private AIContentExternalHandler<FunctionCallContent, FunctionResultContent>? _functionCallHandler;
Expand Down Expand Up @@ -77,6 +78,7 @@ internal void ResetChat(ResetChatSignal signal, IWorkflowContext context)
{
this._session = null;
this._currentTurnEmitEvents = null;
this._currentTurnRunOptions = null;
}

private ValueTask HandleUserInputResponseAsync(
Expand All @@ -99,7 +101,7 @@ private ValueTask HandleUserInputResponseAsync(
MessageId = Guid.NewGuid().ToString("N"),
});

await this.ContinueTurnAsync(pendingMessages, ctx, this._currentTurnEmitEvents ?? false, ct).ConfigureAwait(false);
await this.ContinueTurnAsync(pendingMessages, ctx, this._currentTurnEmitEvents ?? false, ctx.GetAgentRunOptions(this._currentTurnRunOptions), ct).ConfigureAwait(false);

// Clear the buffered turn messages because they were consumed by ContinueTurnAsync.
return null;
Expand Down Expand Up @@ -127,7 +129,7 @@ private ValueTask HandleFunctionResultAsync(
MessageId = Guid.NewGuid().ToString("N"),
});

await this.ContinueTurnAsync(pendingMessages, ctx, this._currentTurnEmitEvents ?? false, ct).ConfigureAwait(false);
await this.ContinueTurnAsync(pendingMessages, ctx, this._currentTurnEmitEvents ?? false, ctx.GetAgentRunOptions(this._currentTurnRunOptions), ct).ConfigureAwait(false);

// Clear the buffered turn messages because they were consumed by ContinueTurnAsync.
return null;
Expand Down Expand Up @@ -176,9 +178,10 @@ protected internal override async ValueTask OnCheckpointRestoredAsync(IWorkflowC
|| (this._functionCallHandler?.HasPendingRequests == true);

// While we save this on the instance, we are not cross-run shareable, but as AgentBinding uses the factory pattern this is not an issue
private async ValueTask ContinueTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool emitEvents, CancellationToken cancellationToken)
private async ValueTask ContinueTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool emitEvents, AgentRunOptions? runOptions, CancellationToken cancellationToken)
{
this._currentTurnEmitEvents = emitEvents;
this._currentTurnRunOptions = runOptions;
if (this._options.ForwardIncomingMessages)
{
await context.SendMessageAsync(messages, cancellationToken).ConfigureAwait(false);
Expand All @@ -188,7 +191,7 @@ private async ValueTask ContinueTurnAsync(List<ChatMessage> messages, IWorkflowC
? messages.Select(m => m.ChatAssistantToUserIfNotFromNamed(this._agent.Name ?? this._agent.Id))
: messages;

AgentResponse response = await this.InvokeAgentAsync(filteredMessages, context, emitEvents, cancellationToken).ConfigureAwait(false);
AgentResponse response = await this.InvokeAgentAsync(filteredMessages, context, emitEvents, runOptions, cancellationToken).ConfigureAwait(false);

// Filter out server-side artifacts (reasoning tokens, web search calls, etc.)
// that are internal to this agent. Forwarding them to other agents in the workflow
Expand All @@ -204,18 +207,27 @@ await context.SendMessageAsync(forwardableMessages, cancellationToken)
// If we have no outstanding requests, we can yield a turn token back to the workflow.
if (!this.HasOutstandingRequests)
{
await context.SendMessageAsync(new TurnToken(this._currentTurnEmitEvents), cancellationToken).ConfigureAwait(false);
await context.SendMessageAsync(new TurnToken(this._currentTurnEmitEvents, this._currentTurnRunOptions), cancellationToken).ConfigureAwait(false);
this._currentTurnEmitEvents = null; // Possibly not actually necessary, but cleaning this up makes it clearer when debugging
this._currentTurnRunOptions = null;
}
}

protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
=> this.ContinueTurnAsync(messages,
context,
TurnExtensions.ShouldEmitStreamingEvents(turnTokenSetting: emitEvents, this._options.EmitAgentUpdateEvents),
runOptions: null,
cancellationToken);

private async ValueTask<AgentResponse> InvokeAgentAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, bool emitUpdateEvents, CancellationToken cancellationToken = default)
protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, TurnToken turnToken, CancellationToken cancellationToken = default)
=> this.ContinueTurnAsync(messages,
context,
turnToken.ShouldEmitStreamingEvents(this._options.EmitAgentUpdateEvents),
context.GetAgentRunOptions(turnToken.RunOptions),
cancellationToken);

private async ValueTask<AgentResponse> InvokeAgentAsync(IEnumerable<ChatMessage> messages, IWorkflowContext context, bool emitUpdateEvents, AgentRunOptions? runOptions, CancellationToken cancellationToken = default)
{
AgentResponse response;
AIAgentUnservicedRequestsCollector collector = new(this._userInputHandler, this._functionCallHandler);
Expand All @@ -226,6 +238,7 @@ private async ValueTask<AgentResponse> InvokeAgentAsync(IEnumerable<ChatMessage>
IAsyncEnumerable<AgentResponseUpdate> agentStream = this._agent.RunStreamingAsync(
messages,
await this.EnsureSessionAsync(context, cancellationToken).ConfigureAwait(false),
runOptions,
cancellationToken: cancellationToken);

List<AgentResponseUpdate> updates = [];
Expand Down Expand Up @@ -281,6 +294,7 @@ await this.EnsureSessionAsync(context, cancellationToken).ConfigureAwait(false),
// Otherwise, run the agent in non-streaming mode.
response = await this._agent.RunAsync(messages,
await this.EnsureSessionAsync(context, cancellationToken).ConfigureAwait(false),
runOptions,
cancellationToken: cancellationToken)
.ConfigureAwait(false);

Expand Down
Loading
Loading