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
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
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(object! sharedStateKey, System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
const Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties.ExecutorId = "executorId" -> string!
Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(object! sharedStateKey, System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
const Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties.ExecutorId = "executorId" -> string!
Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(object! sharedStateKey, System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
const Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties.ExecutorId = "executorId" -> string!
Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(object! sharedStateKey, System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
const Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties.ExecutorId = "executorId" -> string!
Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(object! sharedStateKey, System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
Microsoft.Agents.AI.Workflows.Executor.RegisterCheckpointRestoringCallback(System.Func<Microsoft.Agents.AI.Workflows.IWorkflowContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask>! callback) -> void
const Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties.ExecutorId = "executorId" -> string!
Microsoft.Agents.AI.Workflows.WorkflowAgentAdditionalProperties
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

FYI: If we are unsure about something new that is public we often mark it as experimental, to make sure we can still make changes to it before graduating the feature. We then remove experimental once we are happy with the overall change.
Not sure if that is applicable here, but just an FYI.

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.

Thanks I think Tao Chen (@TaoChenOSU) and Peter Ibekwe (@peibekwe) will be able to inform the design here and whether or not we need to add those experimental aspects.

{
/// <summary>
/// The key for the workflow executor identifier that produced an agent response update.
/// </summary>
public const string ExecutorId = "executorId";
}
28 changes: 19 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,28 @@ 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)
Comment thread
baywet marked this conversation as resolved.
{
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 +509,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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe I'm wrong, but I would have thought that the raw representation would typically be some sort of WorkflowEvent, and if so, that users could get the executor id from that.

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.

I don't think I have enough context to reply to this comment. Why should it be a WorkflowEvent as opposed to an Executor Event? What parameters are you taking into consideration for this design suggestion?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't really know enough about this area either to know all the different raw representation types that might be represented here, but I think it's a good thing to verify before making the change.
Peter Ibekwe (@peibekwe), might have a better sense.

};
}, executorId);

await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false, cancellationToken)
.ConfigureAwait(false)
Expand Down Expand Up @@ -586,7 +596,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 +620,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 @@ -71,6 +71,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 @@ -90,6 +119,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