From 31494ee63f82aea5a057ce3a490d91593b43977c Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Sat, 6 Dec 2025 15:21:17 -0800 Subject: [PATCH 01/10] .NET: Add TTLs to durable agent sessions --- .../durable-agents/durable-agents-ttl.md | 150 +++++++++++++ .../AzureFunctions/01_SingleAgent/Program.cs | 2 +- .../AgentEntity.cs | 105 +++++++++- .../DurableAgentsOptions.cs | 45 +++- .../Microsoft.Agents.AI.DurableTask/Logs.cs | 46 ++++ .../ServiceCollectionExtensions.cs | 3 + .../State/DurableAgentStateData.cs | 7 + .../TimeToLiveTests.cs | 197 ++++++++++++++++++ 8 files changed, 543 insertions(+), 12 deletions(-) create mode 100644 docs/features/durable-agents/durable-agents-ttl.md create mode 100644 dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs diff --git a/docs/features/durable-agents/durable-agents-ttl.md b/docs/features/durable-agents/durable-agents-ttl.md new file mode 100644 index 0000000000..7cdb8de223 --- /dev/null +++ b/docs/features/durable-agents/durable-agents-ttl.md @@ -0,0 +1,150 @@ +# Time-To-Live (TTL) for durable agent sessions + +## Overview + +The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity. + +## What is TTL? + +Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime. + +## Benefits + +- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions +- **Cost optimization**: Reduces storage costs by automatically removing unused session state +- **Resource management**: Prevents unbounded growth of agent session state in storage +- **Configurable**: Set TTL globally or per-agent type to match your application's needs + +## Configuration + +TTL can be configured at two levels: + +1. **Global default TTL**: Applies to all agent sessions unless overridden +2. **Per-agent type TTL**: Overrides the global default for specific agent types + +Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. This prevents excessive deletion operations for agent sessions with short TTLs. + +> [!NOTE] +> Setting the deletion delay is an advanced feature and should only be used if the default value results in excessive deletion operations (in which case you should increase the default value) or if you need to ensure that deletion operations are executed promptly (in which case you should decrease the default value). + +### Default values + +- **Default TTL**: 30 days +- **Minimum TTL deletion delay**: 5 minutes (subject to change in future releases) + +### Configuration examples + +#### .NET + +```csharp +// Configure global default TTL and minimum signal delay +services.ConfigureDurableAgents( + options => + { + // Set global default TTL to 7 days + options.DefaultTimeToLive = TimeSpan.FromDays(7); + + // Set minimum signal delay to 10 minutes + options.MinimumTimeToLiveSignalDelay = TimeSpan.FromMinutes(10); + + // Add agents (will use global default TTL) + options.AddAIAgent(myAgent); + }); + +// Configure per-agent TTL +services.ConfigureDurableAgents( + options => + { + options.DefaultTimeToLive = TimeSpan.FromDays(30); // Global default + + // Agent with custom TTL of 1 day + options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1)); + + // Agent with custom TTL of 90 days + options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90)); + + // Agent using global default (30 days) + options.AddAIAgent(defaultAgent); + }); + +// Disable TTL for specific agents by setting TTL to null +services.ConfigureDurableAgents( + options => + { + options.DefaultTimeToLive = TimeSpan.FromDays(30); + + // Agent with no TTL (never expires) + options.AddAIAgent(permanentAgent, timeToLive: null); + }); +``` + +## How TTL works + +The following sections describe how TTL works in detail. + +### Expiration tracking + +Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message: + +1. When a message is sent to an agent session, the expiration time is set to `current time + TTL` +2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints) +3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time. + +### State deletion + +When an agent session expires, its entire state is deleted, including: + +- Conversation history +- Any custom state data +- Expiration timestamps + +After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history. + +## Behavior examples + +The following examples illustrate how TTL works in different scenarios. + +### Example 1: Agent session expires after TTL + +1. Agent configured with 30-day TTL +2. User sends message at Day 0 → agent session created, expiration set to Day 30 +3. No further messages sent +4. At Day 30 → Agent session is deleted +5. User sends message at Day 31 → New agent session created with fresh conversation history + +### Example 2: TTL reset on interaction + +1. Agent configured with 30-day TTL +2. User sends message at Day 0 → agent session created, expiration set to Day 30 +3. User sends message at Day 15 → Expiration reset to Day 45 +4. User sends message at Day 40 → Expiration reset to Day 70 +5. Agent session remains active as long as there are regular interactions + +## Logging + +The TTL feature includes comprehensive logging to track state changes: + +- **Expiration time updated**: Logged when TTL expiration time is set or updated +- **Deletion scheduled**: Logged when a deletion check signal is scheduled +- **Deletion check**: Logged when a deletion check operation runs +- **Session expired**: Logged when an agent session is deleted due to expiration +- **TTL rescheduled**: Logged when a deletion signal is rescheduled + +These logs help monitor TTL behavior and troubleshoot any issues. + +## Best practices + +1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state. + +2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes. + +3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed. + +4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods. + +## Limitations + +- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp. +- Deletion checks are durably scheduled operations and may have slight delays depending on system load. +- Once an agent session is deleted, its conversation history cannot be recovered. +- TTL deletion requires at least one worker to be available to process the deletion operation message. diff --git a/dotnet/samples/AzureFunctions/01_SingleAgent/Program.cs b/dotnet/samples/AzureFunctions/01_SingleAgent/Program.cs index 60b3103adc..43cadaecc5 100644 --- a/dotnet/samples/AzureFunctions/01_SingleAgent/Program.cs +++ b/dotnet/samples/AzureFunctions/01_SingleAgent/Program.cs @@ -32,6 +32,6 @@ using IHost app = FunctionsApplication .CreateBuilder(args) .ConfigureFunctionsWebApplication() - .ConfigureDurableAgents(options => options.AddAIAgent(agent)) + .ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1))) .Build(); app.Run(); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index 166799a124..fc70fd790b 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -16,6 +16,7 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella private readonly DurableTaskClient _client = services.GetRequiredService(); private readonly ILoggerFactory _loggerFactory = services.GetRequiredService(); private readonly IAgentResponseHandler? _messageHandler = services.GetService(); + private readonly DurableAgentsOptions _options = services.GetRequiredService(); private readonly CancellationToken _cancellationToken = cancellationToken != default ? cancellationToken : services.GetService()?.ApplicationStopping ?? CancellationToken.None; @@ -23,18 +24,11 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella public async Task RunAgentAsync(RunRequest request) { AgentSessionId sessionId = this.Context.Id; - IReadOnlyDictionary> agents = - this._services.GetRequiredService>>(); - if (!agents.TryGetValue(sessionId.Name, out Func? agentFactory)) - { - throw new InvalidOperationException($"Agent '{sessionId.Name}' not found"); - } - - AIAgent agent = agentFactory(this._services); + AIAgent agent = this.GetAgent(sessionId); EntityAgentWrapper agentWrapper = new(agent, this.Context, request, this._services); // Logger category is Microsoft.DurableTask.Agents.{agentName}.{sessionId} - ILogger logger = this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agent.Name}.{sessionId.Key}"); + ILogger logger = this.GetLogger(agent.Name!, sessionId.Key); if (request.Messages.Count == 0) { @@ -113,6 +107,27 @@ async IAsyncEnumerable StreamResultsAsync() response.Usage?.TotalTokenCount); } + // Update TTL expiration time. Only schedule deletion check on first interaction. + // Subsequent interactions just update the expiration time; CheckAndDeleteIfExpiredAsync + // will reschedule the deletion check when it runs. + TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name); + if (timeToLive.HasValue) + { + DateTime newExpirationTime = DateTime.UtcNow.Add(timeToLive.Value); + bool isFirstInteraction = this.State.Data.ExpirationTime is null; + + this.State.Data.ExpirationTime = newExpirationTime; + logger.LogTTLExpirationTimeUpdated(sessionId, newExpirationTime); + + // Only schedule deletion check on the first interaction when entity is created. + // On subsequent interactions, we just update the expiration time. The scheduled + // CheckAndDeleteIfExpiredAsync will reschedule itself if the entity hasn't expired. + if (isFirstInteraction) + { + await this.ScheduleDeletionCheckAsync(sessionId, logger, timeToLive.Value); + } + } + return response; } finally @@ -121,4 +136,76 @@ async IAsyncEnumerable StreamResultsAsync() DurableAgentContext.ClearCurrent(); } } + + /// + /// Checks if the entity has expired and deletes it if so, otherwise reschedules the deletion check. + /// + public Task CheckAndDeleteIfExpiredAsync() + { + AgentSessionId sessionId = this.Context.Id; + AIAgent agent = this.GetAgent(sessionId); + ILogger logger = this.GetLogger(agent.Name!, sessionId.Key); + + DateTime currentTime = DateTime.UtcNow; + DateTime? expirationTime = this.State.Data.ExpirationTime; + + logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime); + + if (expirationTime.HasValue && currentTime >= expirationTime.Value) + { + // Entity has expired, delete it + logger.LogTTLEntityExpired(sessionId, expirationTime.Value); + this.State = null!; + return Task.CompletedTask; + } + + // Entity hasn't expired yet, reschedule the deletion check + if (expirationTime.HasValue) + { + TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name); + if (timeToLive.HasValue) + { + return this.ScheduleDeletionCheckAsync(sessionId, logger, timeToLive.Value); + } + } + + return Task.CompletedTask; + } + + private async Task ScheduleDeletionCheckAsync(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive) + { + DateTime currentTime = DateTime.UtcNow; + DateTime expirationTime = this.State.Data.ExpirationTime ?? currentTime.Add(timeToLive); + TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay; + + // Calculate when to schedule the signal: max of expiration time and current time + minimum delay + DateTime scheduledTime = expirationTime > currentTime.Add(minimumDelay) + ? expirationTime + : currentTime.Add(minimumDelay); + + logger.LogTTLDeletionScheduled(sessionId, scheduledTime); + + // Schedule a signal to self to check for expiration + this.Context.SignalEntity( + this.Context.Id, + nameof(CheckAndDeleteIfExpiredAsync), + options: new SignalEntityOptions { SignalTime = scheduledTime }); + } + + private AIAgent GetAgent(AgentSessionId sessionId) + { + IReadOnlyDictionary> agents = + this._services.GetRequiredService>>(); + if (!agents.TryGetValue(sessionId.Name, out Func? agentFactory)) + { + throw new InvalidOperationException($"Agent '{sessionId.Name}' not found"); + } + + return agentFactory(this._services); + } + + private ILogger GetLogger(string agentName, string sessionKey) + { + return this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agentName}.{sessionKey}"); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs index f2ac3f4c9a..26887e5bdb 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs @@ -9,6 +9,25 @@ public sealed class DurableAgentsOptions { // Agent names are case-insensitive private readonly Dictionary> _agentFactories = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the default time-to-live (TTL) for agent entities. + /// + /// + /// If an agent entity is idle for this duration, it will be automatically deleted. + /// Defaults to 30 days. Set to to disable TTL for agents without explicit TTL configuration. + /// + public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(30); + + /// + /// Gets or sets the minimum delay for scheduling TTL deletion signals. + /// + /// + /// This ensures that deletion signals are not scheduled too frequently. + /// Defaults to 5 minutes. + /// + public TimeSpan MinimumTimeToLiveSignalDelay { get; set; } = TimeSpan.FromMinutes(5); internal DurableAgentsOptions() { @@ -19,13 +38,19 @@ internal DurableAgentsOptions() /// /// The name of the agent. /// The factory function to create the agent. + /// Optional time-to-live for this agent's entities. If not specified, uses . /// The options instance. /// Thrown when or is null. - public DurableAgentsOptions AddAIAgentFactory(string name, Func factory) + public DurableAgentsOptions AddAIAgentFactory(string name, Func factory, TimeSpan? timeToLive = null) { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(factory); this._agentFactories.Add(name, factory); + if (timeToLive.HasValue) + { + this._agentTimeToLive[name] = timeToLive; + } + return this; } @@ -50,12 +75,13 @@ public DurableAgentsOptions AddAIAgents(params IEnumerable agents) /// Adds an AI agent to the options. /// /// The agent to add. + /// Optional time-to-live for this agent's entities. If not specified, uses . /// The options instance. /// Thrown when is null. /// /// Thrown when is null or whitespace or when an agent with the same name has already been registered. /// - public DurableAgentsOptions AddAIAgent(AIAgent agent) + public DurableAgentsOptions AddAIAgent(AIAgent agent, TimeSpan? timeToLive = null) { ArgumentNullException.ThrowIfNull(agent); @@ -70,6 +96,11 @@ public DurableAgentsOptions AddAIAgent(AIAgent agent) } this._agentFactories.Add(agent.Name, sp => agent); + if (timeToLive.HasValue) + { + this._agentTimeToLive[agent.Name] = timeToLive; + } + return this; } @@ -81,4 +112,14 @@ internal IReadOnlyDictionary> GetAgentFa { return this._agentFactories.AsReadOnly(); } + + /// + /// Gets the time-to-live for a specific agent, or the default TTL if not specified. + /// + /// The name of the agent. + /// The time-to-live for the agent, or the default TTL if not specified. + internal TimeSpan? GetTimeToLive(string agentName) + { + return this._agentTimeToLive.TryGetValue(agentName, out TimeSpan? ttl) ? ttl : this.DefaultTimeToLive; + } } diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs index 0bec1e149c..c2e0f201a9 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs @@ -46,4 +46,50 @@ public static partial void LogAgentResponse( Level = LogLevel.Information, Message = "Found response for agent with session ID '{SessionId}' with correlation ID '{CorrelationId}'")] public static partial void LogDonePollingForResponse(this ILogger logger, AgentSessionId sessionId, string correlationId); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "[{SessionId}] TTL expiration time updated to {ExpirationTime:O}")] + public static partial void LogTTLExpirationTimeUpdated( + this ILogger logger, + AgentSessionId sessionId, + DateTime expirationTime); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "[{SessionId}] TTL deletion signal scheduled for {ScheduledTime:O}")] + public static partial void LogTTLDeletionScheduled( + this ILogger logger, + AgentSessionId sessionId, + DateTime scheduledTime); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Information, + Message = "[{SessionId}] TTL deletion check running. Expiration time: {ExpirationTime:O}, Current time: {CurrentTime:O}")] + public static partial void LogTTLDeletionCheck( + this ILogger logger, + AgentSessionId sessionId, + DateTime? expirationTime, + DateTime currentTime); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Information, + Message = "[{SessionId}] Entity expired and deleted due to TTL. Expiration time: {ExpirationTime:O}")] + public static partial void LogTTLEntityExpired( + this ILogger logger, + AgentSessionId sessionId, + DateTime expirationTime); + + [LoggerMessage( + EventId = 10, + Level = LogLevel.Information, + Message = "[{SessionId}] TTL deletion signal rescheduled for {ScheduledTime:O}")] + public static partial void LogTTLRescheduled( + this ILogger logger, + AgentSessionId sessionId, + DateTime scheduledTime); } diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs index 2f435e0541..79d44924ca 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs @@ -85,6 +85,9 @@ internal static DurableAgentsOptions ConfigureDurableAgents( // The agent dictionary contains the real agent factories, which is used by the agent entities. services.AddSingleton(agents); + // Register the options so AgentEntity can access TTL configuration + services.AddSingleton(options); + // The keyed services are used to resolve durable agent *proxy* instances for external clients. foreach (var factory in agents) { diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs index f51820dcf5..7e0befb396 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs @@ -17,6 +17,13 @@ internal sealed class DurableAgentStateData [JsonPropertyName("conversationHistory")] public IList ConversationHistory { get; init; } = []; + /// + /// Gets or sets the expiration time for this agent entity. + /// If the entity is idle beyond this time, it will be automatically deleted. + /// + [JsonPropertyName("expirationTime")] + public DateTime? ExpirationTime { get; set; } + /// /// Gets any additional data found during deserialization that does not map to known properties. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs new file mode 100644 index 0000000000..8bae25de2c --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics; +using System.Reflection; +using Microsoft.Agents.AI.DurableTask.State; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.Extensions.Configuration; +using OpenAI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; + +/// +/// Tests for Time-To-Live (TTL) functionality of durable agent entities. +/// +[Collection("Sequential")] +[Trait("Category", "Integration")] +public sealed class TimeToLiveTests(ITestOutputHelper outputHelper) : IDisposable +{ + private static readonly TimeSpan s_defaultTimeout = Debugger.IsAttached + ? TimeSpan.FromMinutes(5) + : TimeSpan.FromSeconds(30); + + private static readonly IConfiguration s_configuration = + new ConfigurationBuilder() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .AddEnvironmentVariables() + .Build(); + + private readonly ITestOutputHelper _outputHelper = outputHelper; + private readonly CancellationTokenSource _cts = new(delay: s_defaultTimeout); + + private CancellationToken TestTimeoutToken => this._cts.Token; + + public void Dispose() => this._cts.Dispose(); + + [Fact] + public async Task EntityExpiresAfterTTLAsync() + { + // Arrange: Create agent with short TTL (10 seconds) + TimeSpan ttl = TimeSpan.FromSeconds(10); + AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( + name: "TTLTestAgent", + instructions: "You are a helpful assistant." + ); + + using TestHelper testHelper = TestHelper.Start( + this._outputHelper, + options => + { + options.DefaultTimeToLive = ttl; + options.MinimumTimeToLiveSignalDelay = TimeSpan.FromSeconds(1); + options.AddAIAgent(simpleAgent); + }); + + AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); + AgentThread thread = agentProxy.GetNewThread(); + DurableTaskClient client = testHelper.GetClient(); + AgentSessionId sessionId = thread.GetService(); + + // Act: Send a message to the agent + await agentProxy.RunAsync( + message: "Hello!", + thread, + cancellationToken: this.TestTimeoutToken); + + // Verify entity exists and get expiration time + EntityMetadata? entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + Assert.NotNull(entity); + Assert.True(entity.IncludesState); + + DurableAgentState state = entity.State.ReadAs(); + Assert.NotNull(state.Data.ExpirationTime); + DateTime expirationTime = state.Data.ExpirationTime.Value; + Assert.True(expirationTime > DateTime.UtcNow); + + // Calculate how long to wait: expiration time + buffer for signal processing + TimeSpan waitTime = expirationTime - DateTime.UtcNow + TimeSpan.FromSeconds(1); + if (waitTime > TimeSpan.Zero) + { + await Task.Delay(waitTime, this.TestTimeoutToken); + } + + // Poll the entity state until it's deleted (with timeout) + DateTime pollTimeout = DateTime.UtcNow.AddSeconds(10); + bool entityDeleted = false; + while (DateTime.UtcNow < pollTimeout && !entityDeleted) + { + entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + entityDeleted = entity is null; + + if (!entityDeleted) + { + await Task.Delay(TimeSpan.FromSeconds(1), this.TestTimeoutToken); + } + } + + // Assert: Verify entity state is deleted + Assert.True(entityDeleted, "Entity should have been deleted after TTL expiration"); + } + + [Fact] + public async Task EntityTTLResetsOnInteractionAsync() + { + // Arrange: Create agent with short TTL + TimeSpan ttl = TimeSpan.FromSeconds(6); + AIAgent simpleAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent( + name: "TTLResetTestAgent", + instructions: "You are a helpful assistant." + ); + + using TestHelper testHelper = TestHelper.Start( + this._outputHelper, + options => + { + options.DefaultTimeToLive = ttl; + options.MinimumTimeToLiveSignalDelay = TimeSpan.FromSeconds(1); + options.AddAIAgent(simpleAgent); + }); + + AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services); + AgentThread thread = agentProxy.GetNewThread(); + DurableTaskClient client = testHelper.GetClient(); + AgentSessionId sessionId = thread.GetService(); + + // Act: Send first message + await agentProxy.RunAsync( + message: "Hello!", + thread, + cancellationToken: this.TestTimeoutToken); + + EntityMetadata? entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + Assert.NotNull(entity); + Assert.True(entity.IncludesState); + + DurableAgentState state = entity.State.ReadAs(); + DateTime firstExpirationTime = state.Data.ExpirationTime!.Value; + + // Wait partway through TTL + await Task.Delay(TimeSpan.FromSeconds(3), this.TestTimeoutToken); + + // Send second message (should reset TTL) + await agentProxy.RunAsync( + message: "Hello again!", + thread, + cancellationToken: this.TestTimeoutToken); + + // Verify expiration time was updated + entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + Assert.NotNull(entity); + Assert.True(entity.IncludesState); + + state = entity.State.ReadAs(); + DateTime secondExpirationTime = state.Data.ExpirationTime!.Value; + Assert.True(secondExpirationTime > firstExpirationTime); + + // Calculate when the original expiration time would have been + DateTime originalExpirationTime = firstExpirationTime; + TimeSpan waitUntilOriginalExpiration = originalExpirationTime - DateTime.UtcNow + TimeSpan.FromSeconds(2); + + if (waitUntilOriginalExpiration > TimeSpan.Zero) + { + await Task.Delay(waitUntilOriginalExpiration, this.TestTimeoutToken); + } + + // Assert: Entity should still exist because TTL was reset + // The new expiration time should be in the future + entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + Assert.NotNull(entity); + Assert.True(entity.IncludesState); + + state = entity.State.ReadAs(); + Assert.NotNull(state); + Assert.NotNull(state.Data.ExpirationTime); + Assert.True( + state.Data.ExpirationTime > DateTime.UtcNow, + "Entity should still be valid because TTL was reset"); + + // Wait for the entity to be deleted + DateTime pollTimeout = DateTime.UtcNow.AddSeconds(10); + bool entityDeleted = false; + while (DateTime.UtcNow < pollTimeout && !entityDeleted) + { + entity = await client.Entities.GetEntityAsync(sessionId, true, this.TestTimeoutToken); + entityDeleted = entity is null; + + if (!entityDeleted) + { + await Task.Delay(TimeSpan.FromSeconds(1), this.TestTimeoutToken); + } + } + + // Assert: Entity should have been deleted + Assert.True(entityDeleted, "Entity should have been deleted after TTL expiration"); + } +} From 82b1a9c56f433992865fe894c4ba9b2deaafdb22 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 8 Dec 2025 11:19:56 -0800 Subject: [PATCH 02/10] Remove unnecessary async --- .../AgentEntity.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index fc70fd790b..0fd1c484cc 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -124,7 +124,7 @@ async IAsyncEnumerable StreamResultsAsync() // CheckAndDeleteIfExpiredAsync will reschedule itself if the entity hasn't expired. if (isFirstInteraction) { - await this.ScheduleDeletionCheckAsync(sessionId, logger, timeToLive.Value); + this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value); } } @@ -140,7 +140,10 @@ async IAsyncEnumerable StreamResultsAsync() /// /// Checks if the entity has expired and deletes it if so, otherwise reschedules the deletion check. /// - public Task CheckAndDeleteIfExpiredAsync() + /// + /// This method is called by the durable task runtime when a CheckAndDeleteIfExpired signal is received. + /// + public void CheckAndDeleteIfExpired() { AgentSessionId sessionId = this.Context.Id; AIAgent agent = this.GetAgent(sessionId); @@ -151,34 +154,33 @@ public Task CheckAndDeleteIfExpiredAsync() logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime); - if (expirationTime.HasValue && currentTime >= expirationTime.Value) - { - // Entity has expired, delete it - logger.LogTTLEntityExpired(sessionId, expirationTime.Value); - this.State = null!; - return Task.CompletedTask; - } - - // Entity hasn't expired yet, reschedule the deletion check if (expirationTime.HasValue) { - TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name); - if (timeToLive.HasValue) + if (currentTime >= expirationTime.Value) { - return this.ScheduleDeletionCheckAsync(sessionId, logger, timeToLive.Value); + // Entity has expired, delete it + logger.LogTTLEntityExpired(sessionId, expirationTime.Value); + this.State = null!; + } + else + { + // Entity hasn't expired yet, reschedule the deletion check + TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name); + if (timeToLive.HasValue) + { + this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value); + } } } - - return Task.CompletedTask; } - private async Task ScheduleDeletionCheckAsync(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive) + private void ScheduleDeletionCheck(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive) { DateTime currentTime = DateTime.UtcNow; DateTime expirationTime = this.State.Data.ExpirationTime ?? currentTime.Add(timeToLive); TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay; - // Calculate when to schedule the signal: max of expiration time and current time + minimum delay + // To avoid excessive scheduling, we schedule the deletion check for no less than the minimum delay. DateTime scheduledTime = expirationTime > currentTime.Add(minimumDelay) ? expirationTime : currentTime.Add(minimumDelay); @@ -188,7 +190,7 @@ private async Task ScheduleDeletionCheckAsync(AgentSessionId sessionId, ILogger // Schedule a signal to self to check for expiration this.Context.SignalEntity( this.Context.Id, - nameof(CheckAndDeleteIfExpiredAsync), + nameof(CheckAndDeleteIfExpired), // self-signal options: new SignalEntityOptions { SignalTime = scheduledTime }); } From 2257e562ec64b6165aecbbc54c8c2737f8d06cf0 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 8 Dec 2025 16:33:04 -0800 Subject: [PATCH 03/10] PR feedback: clarify UTC --- .../Microsoft.Agents.AI.DurableTask/AgentEntity.cs | 8 ++++---- .../State/DurableAgentStateData.cs | 6 +++--- .../TimeToLiveTests.cs | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index 0fd1c484cc..98c7f60936 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -114,9 +114,9 @@ async IAsyncEnumerable StreamResultsAsync() if (timeToLive.HasValue) { DateTime newExpirationTime = DateTime.UtcNow.Add(timeToLive.Value); - bool isFirstInteraction = this.State.Data.ExpirationTime is null; + bool isFirstInteraction = this.State.Data.ExpirationTimeUtc is null; - this.State.Data.ExpirationTime = newExpirationTime; + this.State.Data.ExpirationTimeUtc = newExpirationTime; logger.LogTTLExpirationTimeUpdated(sessionId, newExpirationTime); // Only schedule deletion check on the first interaction when entity is created. @@ -150,7 +150,7 @@ public void CheckAndDeleteIfExpired() ILogger logger = this.GetLogger(agent.Name!, sessionId.Key); DateTime currentTime = DateTime.UtcNow; - DateTime? expirationTime = this.State.Data.ExpirationTime; + DateTime? expirationTime = this.State.Data.ExpirationTimeUtc; logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime); @@ -177,7 +177,7 @@ public void CheckAndDeleteIfExpired() private void ScheduleDeletionCheck(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive) { DateTime currentTime = DateTime.UtcNow; - DateTime expirationTime = this.State.Data.ExpirationTime ?? currentTime.Add(timeToLive); + DateTime expirationTime = this.State.Data.ExpirationTimeUtc ?? currentTime.Add(timeToLive); TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay; // To avoid excessive scheduling, we schedule the deletion check for no less than the minimum delay. diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs index 7e0befb396..745f619f48 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/State/DurableAgentStateData.cs @@ -18,11 +18,11 @@ internal sealed class DurableAgentStateData public IList ConversationHistory { get; init; } = []; /// - /// Gets or sets the expiration time for this agent entity. + /// Gets or sets the expiration time (UTC) for this agent entity. /// If the entity is idle beyond this time, it will be automatically deleted. /// - [JsonPropertyName("expirationTime")] - public DateTime? ExpirationTime { get; set; } + [JsonPropertyName("expirationTimeUtc")] + public DateTime? ExpirationTimeUtc { get; set; } /// /// Gets any additional data found during deserialization that does not map to known properties. diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs index 8bae25de2c..e683ef3073 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs @@ -71,8 +71,8 @@ await agentProxy.RunAsync( Assert.True(entity.IncludesState); DurableAgentState state = entity.State.ReadAs(); - Assert.NotNull(state.Data.ExpirationTime); - DateTime expirationTime = state.Data.ExpirationTime.Value; + Assert.NotNull(state.Data.ExpirationTimeUtc); + DateTime expirationTime = state.Data.ExpirationTimeUtc.Value; Assert.True(expirationTime > DateTime.UtcNow); // Calculate how long to wait: expiration time + buffer for signal processing @@ -135,7 +135,7 @@ await agentProxy.RunAsync( Assert.True(entity.IncludesState); DurableAgentState state = entity.State.ReadAs(); - DateTime firstExpirationTime = state.Data.ExpirationTime!.Value; + DateTime firstExpirationTime = state.Data.ExpirationTimeUtc!.Value; // Wait partway through TTL await Task.Delay(TimeSpan.FromSeconds(3), this.TestTimeoutToken); @@ -152,7 +152,7 @@ await agentProxy.RunAsync( Assert.True(entity.IncludesState); state = entity.State.ReadAs(); - DateTime secondExpirationTime = state.Data.ExpirationTime!.Value; + DateTime secondExpirationTime = state.Data.ExpirationTimeUtc!.Value; Assert.True(secondExpirationTime > firstExpirationTime); // Calculate when the original expiration time would have been @@ -172,9 +172,9 @@ await agentProxy.RunAsync( state = entity.State.ReadAs(); Assert.NotNull(state); - Assert.NotNull(state.Data.ExpirationTime); + Assert.NotNull(state.Data.ExpirationTimeUtc); Assert.True( - state.Data.ExpirationTime > DateTime.UtcNow, + state.Data.ExpirationTimeUtc > DateTime.UtcNow, "Entity should still be valid because TTL was reset"); // Wait for the entity to be deleted From db8b5854e8d84853df954c9b39911c828476b76f Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 8 Dec 2025 16:53:26 -0800 Subject: [PATCH 04/10] PR feedback: limit minimum signal delay to <= 5 minutes --- .../durable-agents/durable-agents-ttl.md | 11 +++---- .../DurableAgentsOptions.cs | 33 +++++++++++++++---- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/features/durable-agents/durable-agents-ttl.md b/docs/features/durable-agents/durable-agents-ttl.md index 7cdb8de223..81885f8e0b 100644 --- a/docs/features/durable-agents/durable-agents-ttl.md +++ b/docs/features/durable-agents/durable-agents-ttl.md @@ -22,15 +22,15 @@ TTL can be configured at two levels: 1. **Global default TTL**: Applies to all agent sessions unless overridden 2. **Per-agent type TTL**: Overrides the global default for specific agent types -Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. This prevents excessive deletion operations for agent sessions with short TTLs. +Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes. > [!NOTE] -> Setting the deletion delay is an advanced feature and should only be used if the default value results in excessive deletion operations (in which case you should increase the default value) or if you need to ensure that deletion operations are executed promptly (in which case you should decrease the default value). +> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution. ### Default values - **Default TTL**: 30 days -- **Minimum TTL deletion delay**: 5 minutes (subject to change in future releases) +- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases) ### Configuration examples @@ -43,10 +43,7 @@ services.ConfigureDurableAgents( { // Set global default TTL to 7 days options.DefaultTimeToLive = TimeSpan.FromDays(7); - - // Set minimum signal delay to 10 minutes - options.MinimumTimeToLiveSignalDelay = TimeSpan.FromMinutes(10); - + // Add agents (will use global default TTL) options.AddAIAgent(myAgent); }); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs index 26887e5bdb..fcdfd978c6 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs @@ -11,6 +11,12 @@ public sealed class DurableAgentsOptions private readonly Dictionary> _agentFactories = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase); + private TimeSpan _minimumTimeToLiveSignalDelay = TimeSpan.FromMinutes(5); + + internal DurableAgentsOptions() + { + } + /// /// Gets or sets the default time-to-live (TTL) for agent entities. /// @@ -21,16 +27,31 @@ public sealed class DurableAgentsOptions public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(30); /// - /// Gets or sets the minimum delay for scheduling TTL deletion signals. + /// Gets or sets the minimum delay for scheduling TTL deletion signals. Defaults to 5 minutes. /// /// - /// This ensures that deletion signals are not scheduled too frequently. - /// Defaults to 5 minutes. + /// This property is primarily useful for testing (where shorter delays are needed) or for + /// shorter-lived agents in workflows that need more rapid cleanup. The maximum allowed value is 5 minutes. + /// Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. + /// However, this can also increase the load on the system and should be used with caution. /// - public TimeSpan MinimumTimeToLiveSignalDelay { get; set; } = TimeSpan.FromMinutes(5); - - internal DurableAgentsOptions() + /// Thrown when the value exceeds 5 minutes. + public TimeSpan MinimumTimeToLiveSignalDelay { + get => this._minimumTimeToLiveSignalDelay; + set + { + const int MaximumDelayMinutes = 5; + if (value > TimeSpan.FromMinutes(MaximumDelayMinutes)) + { + throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"The minimum time-to-live signal delay cannot exceed {MaximumDelayMinutes} minutes."); + } + + this._minimumTimeToLiveSignalDelay = value; + } } /// From 70cfd58745b8028246ae83487646c375aa3c0738 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Tue, 9 Dec 2025 13:01:40 -0800 Subject: [PATCH 05/10] PR feedback: Fix TTL disablement --- .../src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs | 9 +++++++++ dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index 98c7f60936..44672114bb 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -127,6 +127,15 @@ async IAsyncEnumerable StreamResultsAsync() this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value); } } + else + { + // TTL is disabled. Clear the expiration time if it was previously set. + if (this.State.Data.ExpirationTimeUtc.HasValue) + { + logger.LogTTLExpirationTimeCleared(sessionId); + this.State.Data.ExpirationTimeUtc = null; + } + } return response; } diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs index c2e0f201a9..ba310441df 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs @@ -92,4 +92,12 @@ public static partial void LogTTLRescheduled( this ILogger logger, AgentSessionId sessionId, DateTime scheduledTime); + + [LoggerMessage( + EventId = 11, + Level = LogLevel.Information, + Message = "[{SessionId}] TTL expiration time cleared (TTL disabled)")] + public static partial void LogTTLExpirationTimeCleared( + this ILogger logger, + AgentSessionId sessionId); } From 28abfbe1af1311ee04bcea4e67562450d998b570 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Thu, 11 Dec 2025 16:59:27 -0800 Subject: [PATCH 06/10] Linter: use auto-property --- .../DurableAgentsOptions.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs index fcdfd978c6..1e1d377f7c 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs @@ -11,8 +11,6 @@ public sealed class DurableAgentsOptions private readonly Dictionary> _agentFactories = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase); - private TimeSpan _minimumTimeToLiveSignalDelay = TimeSpan.FromMinutes(5); - internal DurableAgentsOptions() { } @@ -38,7 +36,7 @@ internal DurableAgentsOptions() /// Thrown when the value exceeds 5 minutes. public TimeSpan MinimumTimeToLiveSignalDelay { - get => this._minimumTimeToLiveSignalDelay; + get; set { const int MaximumDelayMinutes = 5; @@ -50,9 +48,9 @@ public TimeSpan MinimumTimeToLiveSignalDelay $"The minimum time-to-live signal delay cannot exceed {MaximumDelayMinutes} minutes."); } - this._minimumTimeToLiveSignalDelay = value; + field = value; } - } + } = TimeSpan.FromMinutes(5); /// /// Adds an AI agent factory to the options. From 85a8064016868d5c02cf071f36bfe86be0c1bab0 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Thu, 11 Dec 2025 17:31:43 -0800 Subject: [PATCH 07/10] Fix build break from OpenAI SDK change --- .../TimeToLiveTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs index e683ef3073..25d40a1c5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs @@ -6,7 +6,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.Configuration; -using OpenAI; +using OpenAI.Chat; using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; From c14b8b64ac162ad1585dafdba3adaef6e8284331 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 12 Dec 2025 10:18:24 -0800 Subject: [PATCH 08/10] Updated CHANGELOG.md --- dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md index d2cdc7cd41..0f66ddc14c 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## [Unreleased] + +- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679)) + ## v1.0.0-preview.251204.1 - Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137)) From 9c56bf29d8f2c8a4ff62c4384aea0994d4708384 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 12 Dec 2025 15:57:16 -0800 Subject: [PATCH 09/10] PR feedback --- dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs index 44672114bb..fe26f176a1 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs @@ -33,6 +33,7 @@ public async Task RunAgentAsync(RunRequest request) if (request.Messages.Count == 0) { logger.LogInformation("Ignoring empty request"); + return new AgentRunResponse(); } this.State.Data.ConversationHistory.Add(DurableAgentStateRequest.FromRunRequest(request)); From 6279e72bb4445d8da098d8d80d075e138bc8fd88 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 15 Dec 2025 14:35:00 -0800 Subject: [PATCH 10/10] Reduce default TTL to 14 days to work around DTS bug --- docs/features/durable-agents/durable-agents-ttl.md | 8 ++++---- .../DurableAgentsOptions.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/features/durable-agents/durable-agents-ttl.md b/docs/features/durable-agents/durable-agents-ttl.md index 81885f8e0b..1a4a4e32d6 100644 --- a/docs/features/durable-agents/durable-agents-ttl.md +++ b/docs/features/durable-agents/durable-agents-ttl.md @@ -29,7 +29,7 @@ Additionally, you can configure a **minimum deletion delay** that controls how f ### Default values -- **Default TTL**: 30 days +- **Default TTL**: 14 days - **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases) ### Configuration examples @@ -52,7 +52,7 @@ services.ConfigureDurableAgents( services.ConfigureDurableAgents( options => { - options.DefaultTimeToLive = TimeSpan.FromDays(30); // Global default + options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default // Agent with custom TTL of 1 day options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1)); @@ -60,7 +60,7 @@ services.ConfigureDurableAgents( // Agent with custom TTL of 90 days options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90)); - // Agent using global default (30 days) + // Agent using global default (14 days) options.AddAIAgent(defaultAgent); }); @@ -68,7 +68,7 @@ services.ConfigureDurableAgents( services.ConfigureDurableAgents( options => { - options.DefaultTimeToLive = TimeSpan.FromDays(30); + options.DefaultTimeToLive = TimeSpan.FromDays(14); // Agent with no TTL (never expires) options.AddAIAgent(permanentAgent, timeToLive: null); diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs index 1e1d377f7c..cefcad323a 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs @@ -20,9 +20,9 @@ internal DurableAgentsOptions() /// /// /// If an agent entity is idle for this duration, it will be automatically deleted. - /// Defaults to 30 days. Set to to disable TTL for agents without explicit TTL configuration. + /// Defaults to 14 days. Set to to disable TTL for agents without explicit TTL configuration. /// - public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(30); + public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(14); /// /// Gets or sets the minimum delay for scheduling TTL deletion signals. Defaults to 5 minutes.