Skip to content
Open
11 changes: 7 additions & 4 deletions dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public sealed class AgentResponseEvent : WorkflowOutputEvent
/// </summary>
/// <param name="executorId">The identifier of the executor that generated this event.</param>
/// <param name="response">The agent response.</param>
public AgentResponseEvent(string executorId, AgentResponse response) : base(response, executorId)
public AgentResponseEvent(string executorId, AgentResponse response) : this(executorId, response, tags: null)
{
this.Response = Throw.IfNull(response);
}

/// <summary>
Expand All @@ -26,9 +25,8 @@ public AgentResponseEvent(string executorId, AgentResponse response) : base(resp
/// <param name="executorId">The identifier of the executor that generated this event.</param>
/// <param name="response">The agent response.</param>
/// <param name="tag">The output tag to associate with this event.</param>
public AgentResponseEvent(string executorId, AgentResponse response, OutputTag tag) : base(response, executorId, tag)
public AgentResponseEvent(string executorId, AgentResponse response, OutputTag tag) : this(executorId, response, [tag])
{
this.Response = Throw.IfNull(response);
}

/// <summary>
Expand All @@ -40,6 +38,11 @@ public AgentResponseEvent(string executorId, AgentResponse response, OutputTag t
public AgentResponseEvent(string executorId, AgentResponse response, IEnumerable<OutputTag>? tags) : base(response, executorId, tags)
{
this.Response = Throw.IfNull(response);
if (!string.IsNullOrEmpty(executorId))
{
this.Response.AdditionalProperties ??= [];
this.Response.AdditionalProperties[WorkflowAgentAdditionalProperties.ExecutorId] = executorId;
Comment thread
baywet marked this conversation as resolved.
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public sealed class AgentResponseUpdateEvent : WorkflowOutputEvent
/// </summary>
/// <param name="executorId">The identifier of the executor that generated this event.</param>
/// <param name="update">The agent run response update.</param>
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : base(update, executorId)
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : this(executorId, update, tags: null)
{
this.Update = Throw.IfNull(update);
}

/// <summary>
Expand All @@ -26,9 +25,8 @@ public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) :
/// <param name="executorId">The identifier of the executor that generated this event.</param>
/// <param name="update">The agent run response update.</param>
/// <param name="tag">The output tag to associate with this event.</param>
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, OutputTag tag) : base(update, executorId, tag)
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, OutputTag tag) : this(executorId, update, [tag])
{
this.Update = Throw.IfNull(update);
}

/// <summary>
Expand All @@ -40,6 +38,11 @@ public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, O
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, IEnumerable<OutputTag>? tags) : base(update, executorId, tags)
{
this.Update = Throw.IfNull(update);
if (!string.IsNullOrEmpty(executorId))
{
this.Update.AdditionalProperties ??= [];
this.Update.AdditionalProperties[WorkflowAgentAdditionalProperties.ExecutorId] = executorId;
Comment thread
baywet marked this conversation as resolved.
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

namespace Microsoft.Agents.AI.Workflows;

/// <summary>
/// Defines additional property keys used by workflow-hosted agents.
/// </summary>
public static class WorkflowAgentAdditionalProperties
{
/// <summary>
/// The key for the workflow executor identifier that produced an agent response update.
/// </summary>
public const string ExecutorId = "executorId";
}
27 changes: 18 additions & 9 deletions dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,27 @@ public AgentResponseUpdate CreateUpdate(string responseId, object raw, params AI
};
}

public AgentResponseUpdate CreateUpdate(string responseId, object raw, ChatMessage message)
public AgentResponseUpdate CreateUpdate(string responseId, object raw, ChatMessage message, string? executorId = default)
{
Throw.IfNull(message);

return new(message.Role, message.Contents)
return SetExecutorId(new(message.Role, message.Contents)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The behaviour pre-existed this change, Tao Chen (@TaoChenOSU) to confirm whether we should fix that or not

{
AuthorName = message.AuthorName,
CreatedAt = message.CreatedAt ?? DateTimeOffset.UtcNow,
MessageId = message.MessageId ?? Guid.NewGuid().ToString("N"),
ResponseId = responseId,
RawRepresentation = raw
};
}, executorId);
}
private static AgentResponseUpdate SetExecutorId(AgentResponseUpdate update, string? executorId)
{
if (!string.IsNullOrEmpty(executorId))
{
update.AdditionalProperties ??= [];
update.AdditionalProperties[WorkflowAgentAdditionalProperties.ExecutorId] = executorId;
}
return update;
}

private async ValueTask<ResumeRunResult> CreateOrResumeRunAsync(List<ChatMessage> messages, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -499,15 +508,15 @@ IAsyncEnumerable<AgentResponseUpdate> InvokeStageAsync(
await run.TrySendMessageAsync(new TurnToken(emitEvents: true)).ConfigureAwait(false);
}

AgentResponseUpdate CreateObservabilityUpdate(WorkflowEvent evt)
=> new(ChatRole.Assistant, [])
AgentResponseUpdate CreateObservabilityUpdate(WorkflowEvent evt, string? executorId = default)
=> SetExecutorId(new(ChatRole.Assistant, [])
{
CreatedAt = DateTimeOffset.UtcNow,
MessageId = Guid.NewGuid().ToString("N"),
Role = ChatRole.Assistant,
ResponseId = this.LastResponseId,
RawRepresentation = evt
};
}, executorId);

await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false, cancellationToken)
.ConfigureAwait(false)
Expand Down Expand Up @@ -586,7 +595,7 @@ AgentResponseUpdate CreateObservabilityUpdate(WorkflowEvent evt)
// the legacy default, keep today's behavior — gated by the include flag.
if (!Futures.EnableAgentResponseOutputTaggingAndFiltering && !this._includeWorkflowOutputsInResponse)
{
yield return CreateObservabilityUpdate(evt);
yield return CreateObservabilityUpdate(evt, agentResponse.ExecutorId);
break;
}

Expand All @@ -610,13 +619,13 @@ AgentResponseUpdate CreateObservabilityUpdate(WorkflowEvent evt)
}

emittedMessage = true;
yield return this.CreateUpdate(this.LastResponseId, evt, message);
yield return this.CreateUpdate(this.LastResponseId, evt, message, agentResponse.ExecutorId);
}
if (!emittedMessage && suppressedStreamedMessage)
{
// Preserve the completion event for observability after its correlated
// streamed content has already been forwarded.
yield return CreateObservabilityUpdate(evt);
yield return CreateObservabilityUpdate(evt, agentResponse.ExecutorId);
}
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ public void AgentResponseUpdateEvent_IsWorkflowOutputEvent()
Assert.Same(update, evt.Data);
}

/// <summary>
/// Verifies that AgentResponseUpdateEvent annotates updates with the authoritative workflow executor identifier.
/// </summary>
[Fact]
public void AgentResponseUpdateEvent_OverwritesExecutorIdAdditionalProperty()
{
// Arrange
const string ExecutorId = "executor1";
const string ProviderExecutorId = "provider-controlled-executor";
const string ExistingMetadataKey = "provider-metadata";
AgentResponseUpdate update = new(ChatRole.Assistant, "test")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
[ExistingMetadataKey] = "provider metadata",
[WorkflowAgentAdditionalProperties.ExecutorId] = ProviderExecutorId,
},
};

// Act
AgentResponseUpdateEvent evt = new(ExecutorId, update);

// Assert
Assert.Same(update, evt.Update);
Assert.NotNull(update.AdditionalProperties);
Assert.Equal("provider metadata", update.AdditionalProperties[ExistingMetadataKey]);
Assert.Equal(ExecutorId, update.AdditionalProperties[WorkflowAgentAdditionalProperties.ExecutorId]);
}

/// <summary>
/// Verifies that AgentResponseEvent inherits from WorkflowOutputEvent.
/// </summary>
Expand All @@ -91,6 +120,35 @@ public void AgentResponseEvent_IsWorkflowOutputEvent()
Assert.Same(response, evt.Data);
}

/// <summary>
/// Verifies that AgentResponseEvent annotates responses with the authoritative workflow executor identifier.
/// </summary>
[Fact]
public void AgentResponseEvent_OverwritesExecutorIdAdditionalProperty()
{
// Arrange
const string ExecutorId = "executor1";
const string ProviderExecutorId = "provider-controlled-executor";
const string ExistingMetadataKey = "provider-metadata";
AgentResponse response = new(new List<ChatMessage> { new(ChatRole.Assistant, "test") })
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
[ExistingMetadataKey] = "provider metadata",
[WorkflowAgentAdditionalProperties.ExecutorId] = ProviderExecutorId,
},
};

// Act
AgentResponseEvent evt = new(ExecutorId, response);

// Assert
Assert.Same(response, evt.Response);
Assert.NotNull(response.AdditionalProperties);
Assert.Equal("provider metadata", response.AdditionalProperties[ExistingMetadataKey]);
Assert.Equal(ExecutorId, response.AdditionalProperties[WorkflowAgentAdditionalProperties.ExecutorId]);
}

/// <summary>
/// Verifies that WorkflowStartedEvent is emitted first before any SuperStepStartedEvent.
/// </summary>
Expand Down
Loading
Loading