Merge branch 'main' into feat/durable_task

This commit is contained in:
Shyju Krishnankutty
2026-03-13 11:34:11 -07:00
committed by GitHub
Unverified
3 changed files with 169 additions and 1 deletions
@@ -33,9 +33,17 @@
## v1.0.0-preview.251219.1
- Filter empty `AIContent` from durable agent state responses ([#4670](https://github.com/microsoft/agent-framework/pull/4670))
## v1.0.0-preview.260311.1
### Changed
- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
NOTE: Some of the above changes may have been part of earlier releases not mentioned in this file.
## v1.0.0-preview.251204.1
- Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137))
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask.State;
@@ -28,7 +29,10 @@ internal sealed class DurableAgentStateResponse : DurableAgentStateEntry
{
CorrelationId = correlationId,
CreatedAt = response.CreatedAt ?? response.Messages.Max(m => m.CreatedAt) ?? DateTimeOffset.UtcNow,
Messages = response.Messages.Select(DurableAgentStateMessage.FromChatMessage).ToList(),
Messages = response.Messages
.Where(HasSerializableContent)
.Select(DurableAgentStateMessage.FromChatMessage)
.ToList(),
Usage = DurableAgentStateUsage.FromUsage(response.Usage)
};
}
@@ -46,4 +50,18 @@ internal sealed class DurableAgentStateResponse : DurableAgentStateEntry
Usage = this.Usage?.ToUsageDetails(),
};
}
// Checks whether a ChatMessage has any content that will produce meaningful serialized data.
// Known derived AIContent types (TextContent, FunctionCallContent, etc.) are always serializable.
// Base AIContent instances only carry RawRepresentation (which is [JsonIgnore]), Annotations, and
// AdditionalProperties. We keep the message if any base AIContent has annotations or additional
// properties set. NOTE: if AIContent gains new serializable properties in the future, this check
// should be updated accordingly.
private static bool HasSerializableContent(ChatMessage message)
{
return message.Contents.Any(c =>
c.GetType() != typeof(AIContent) ||
c.Annotations?.Count > 0 ||
c.AdditionalProperties?.Count > 0);
}
}
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask.State;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask.Tests.Unit.State;
public sealed class DurableAgentStateResponseTests
{
[Fact]
public void FromResponseDropsMessagesContainingOnlyOpaqueContent()
{
// Arrange: one message with real text, one with only opaque AIContent
ChatMessage usefulMessage = new(ChatRole.Assistant, "Hello, world!")
{
CreatedAt = DateTimeOffset.UtcNow
};
ChatMessage opaqueOnlyMessage = new(ChatRole.Assistant, [
new AIContent
{
RawRepresentation = new { kind = "sessionEvent", sessionId = "s123" }
}])
{
CreatedAt = DateTimeOffset.UtcNow.AddSeconds(1)
};
AgentResponse response = new(new List<ChatMessage> { usefulMessage, opaqueOnlyMessage })
{
CreatedAt = DateTimeOffset.UtcNow
};
// Act
DurableAgentStateResponse durableResponse = DurableAgentStateResponse.FromResponse("corr-123", response);
// Assert: only the useful message survives
DurableAgentStateMessage durableMessage = Assert.Single(durableResponse.Messages);
Assert.Equal(ChatRole.Assistant.Value, durableMessage.Role);
// Round-trip to verify the content is correct
AgentResponse convertedResponse = durableResponse.ToResponse();
ChatMessage convertedMessage = Assert.Single(convertedResponse.Messages);
TextContent textContent = Assert.IsType<TextContent>(Assert.Single(convertedMessage.Contents));
Assert.Equal("Hello, world!", textContent.Text);
}
[Fact]
public void FromResponseKeepsMessagesWithMixedContent()
{
// Arrange: one message with both real text and opaque AIContent
ChatMessage mixedMessage = new(ChatRole.Assistant, [
new TextContent("Some useful text"),
new AIContent { RawRepresentation = new { kind = "metadata" } }])
{
CreatedAt = DateTimeOffset.UtcNow
};
AgentResponse response = new(new List<ChatMessage> { mixedMessage })
{
CreatedAt = DateTimeOffset.UtcNow
};
// Act
DurableAgentStateResponse durableResponse = DurableAgentStateResponse.FromResponse("corr-456", response);
// Assert: the message is kept because it contains at least one serializable content
DurableAgentStateMessage durableMessage = Assert.Single(durableResponse.Messages);
Assert.Equal(ChatRole.Assistant.Value, durableMessage.Role);
}
[Fact]
public void FromResponseDropsAllMessagesWhenAllAreOpaque()
{
// Arrange: all messages contain only opaque AIContent
ChatMessage opaque1 = new(ChatRole.Assistant, [
new AIContent { RawRepresentation = new { kind = "event1" } }])
{
CreatedAt = DateTimeOffset.UtcNow
};
ChatMessage opaque2 = new(ChatRole.Assistant, [
new AIContent { RawRepresentation = new { kind = "event2" } }])
{
CreatedAt = DateTimeOffset.UtcNow.AddSeconds(1)
};
AgentResponse response = new(new List<ChatMessage> { opaque1, opaque2 })
{
CreatedAt = DateTimeOffset.UtcNow
};
// Act
DurableAgentStateResponse durableResponse = DurableAgentStateResponse.FromResponse("corr-789", response);
// Assert: no messages stored
Assert.Empty(durableResponse.Messages);
}
[Fact]
public void FromResponseKeepsBaseAIContentWithAnnotations()
{
// Arrange: base AIContent with annotations should be kept
AIContent contentWithAnnotations = new()
{
RawRepresentation = new { kind = "event" },
Annotations = [new AIAnnotation() { AdditionalProperties = new() { ["cite"] = "ref-1" } }]
};
ChatMessage message = new(ChatRole.Assistant, [contentWithAnnotations])
{
CreatedAt = DateTimeOffset.UtcNow
};
AgentResponse response = new([message]) { CreatedAt = DateTimeOffset.UtcNow };
// Act
DurableAgentStateResponse durableResponse = DurableAgentStateResponse.FromResponse("corr-ann", response);
// Assert: message is kept because the AIContent has annotations
Assert.Single(durableResponse.Messages);
}
[Fact]
public void FromResponseKeepsBaseAIContentWithAdditionalProperties()
{
// Arrange: base AIContent with additional properties should be kept
AIContent contentWithProps = new()
{
RawRepresentation = new { kind = "event" },
AdditionalProperties = new() { ["custom_key"] = "custom_value" }
};
ChatMessage message = new(ChatRole.Assistant, [contentWithProps])
{
CreatedAt = DateTimeOffset.UtcNow
};
AgentResponse response = new([message]) { CreatedAt = DateTimeOffset.UtcNow };
// Act
DurableAgentStateResponse durableResponse = DurableAgentStateResponse.FromResponse("corr-props", response);
// Assert: message is kept because the AIContent has additional properties
Assert.Single(durableResponse.Messages);
}
}