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
8 changes: 5 additions & 3 deletions docs/decisions/0039-shared-agent-session-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Chosen option: **Promote the Foundry Hosting contract to `Microsoft.Agents.AI.Ab
- The abstraction and every public implementation start as experimental under diagnostic `MAAI001`.
- `GetSessionAsync` returns `AgentSession?` and returns `null` when no session is stored.
- `GetOrCreateSessionAsync` performs the explicit lookup or creation operation.
- `SaveSessionAsync` and both lookup methods receive an `AgentSessionStoreKey`.
- `SaveSessionAsync`, `DeleteSessionAsync`, and both lookup methods receive an `AgentSessionStoreKey`.
- `AgentSessionStoreKey.SessionId` identifies the logical session.
- `AgentSessionStoreKey.Partitions` holds zero or more named isolation dimensions. Every partition is
part of identity and implementations cannot ignore unknown partitions.
Expand All @@ -50,7 +50,9 @@ Chosen option: **Promote the Foundry Hosting contract to `Microsoft.Agents.AI.Ab
- Partition order does not affect identity. Physical encoding remains the responsibility of each store.
- `GetService(Type, object?)` and `GetService<TService>(object?)` retain service discovery from conventional
Hosting. Stores can expose themselves, underlying implementations, or additional capabilities.
- `DeleteSessionAsync` is not part of the shared contract.
- `DeleteSessionAsync` is abstract, like `GetSessionAsync` and `SaveSessionAsync`, so the store remains the
single owner of the whole session lifecycle. It removes the stored session and treats a missing session as
a no-op. Decorators forward it, applying the same key transformation used for lookup and save.

The duplicate types in `Microsoft.Agents.AI.Hosting` and `Microsoft.Agents.AI.Foundry.Hosting` are removed.
Both packages reference the shared type directly.
Expand Down Expand Up @@ -86,7 +88,7 @@ Negative:

- This is a source-breaking change for implementations of the preview Hosting contract.
- Callers must construct an `AgentSessionStoreKey`; unpartitioned sessions use only `SessionId`.
- Consumers that need deletion must use a storage-specific API until a separate shared deletion capability is defined.
- Every store implementation must provide `DeleteSessionAsync`.

## More Information

Expand Down
8 changes: 7 additions & 1 deletion docs/specs/003-dotnet-hosting-protocol-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ public abstract class AgentSessionStore
AgentSessionStoreKey key,
AgentSession session,
CancellationToken cancellationToken = default);

public abstract ValueTask DeleteSessionAsync(
AIAgent agent,
AgentSessionStoreKey key,
CancellationToken cancellationToken = default);
}
```

Expand Down Expand Up @@ -139,7 +144,8 @@ state without observing another branch's changes. The store performs no cross-ca
application that needs concurrent runs against the same id to be serialized owns that coordination.
`SaveSessionAsync(agent, key, session)` persists the post-run state, including under a newly
minted `resp_*` id when the protocol creates a continuation id. No agent-side holder is needed because
the convenience method already performs lookup or creation.
the convenience method already performs lookup or creation. `DeleteSessionAsync(agent, key)` removes the
stored session and is a no-op when nothing is stored.

`AgentSessionStoreKey` contains a session id plus arbitrary named partitions. Every partition contributes
to identity, independent of dictionary order. Stores must not ignore unknown partitions. Physical key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ public virtual async ValueTask<AgentSession> GetOrCreateSessionAsync(
?? await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Deletes a stored agent session, if present.
/// </summary>
/// <param name="agent">The agent that owns this session.</param>
/// <param name="key">The key that identifies and partitions the session.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
/// <remarks>
/// Implementations delete the session and treat a missing session as a no-op.
/// </remarks>
public abstract ValueTask DeleteSessionAsync(
AIAgent agent,
AgentSessionStoreKey key,
CancellationToken cancellationToken = default);

/// <summary>Asks the store for an object of the specified type.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that identifies the requested service.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetService(System.Type! serviceType, object? serviceKey = null) -> object?
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession?>
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession!>
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? partitions = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetService(System.Type! serviceType, object? serviceKey = null) -> object?
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession?>
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession!>
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? partitions = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetService(System.Type! serviceType, object? serviceKey = null) -> object?
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession?>
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession!>
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? partitions = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetService(System.Type! serviceType, object? serviceKey = null) -> object?
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession?>
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession!>
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? partitions = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetService(System.Type! serviceType, object? serviceKey = null) -> object?
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.GetSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession?>
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.SaveSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, Microsoft.Agents.AI.AgentSession! session, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]abstract Microsoft.Agents.AI.AgentSessionStore.DeleteSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
[MAAI001]virtual Microsoft.Agents.AI.AgentSessionStore.GetOrCreateSessionAsync(Microsoft.Agents.AI.AIAgent! agent, Microsoft.Agents.AI.AgentSessionStoreKey! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.AgentSession!>
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey
[MAAI001]Microsoft.Agents.AI.AgentSessionStoreKey.AgentSessionStoreKey(string! sessionId, System.Collections.Generic.IReadOnlyDictionary<string!, string!>? partitions = null) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Foundry.Hosting;

Expand Down Expand Up @@ -236,6 +237,27 @@ private string BuildNotWritableMessage(string sessionFilePath) =>
return await agent.DeserializeSessionAsync(element, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public override ValueTask DeleteSessionAsync(
AIAgent agent,
AgentSessionStoreKey key,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(agent);
_ = Throw.IfNull(key);
cancellationToken.ThrowIfCancellationRequested();

string path = this.GetSessionPath(agent, key);

// No directory for this agent means nothing was ever stored.
if (Directory.Exists(Path.GetDirectoryName(path)))
{
File.Delete(path);
}

return default;
}

private string GetSessionPath(AIAgent agent, AgentSessionStoreKey key)
{
// The stable key incorporates the session identifier and every partition without exposing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using System.Threading.Tasks;
using Azure.AI.AgentServer.Core.Storage;
using Azure.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

Expand Down Expand Up @@ -63,6 +65,7 @@ public sealed class FoundryAgentSessionStore : AgentSessionStore
private const string KeyField = "key";

private readonly FoundryStateStoreBinding _binding;
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="FoundryAgentSessionStore"/> class.
Expand All @@ -83,15 +86,18 @@ public sealed class FoundryAgentSessionStore : AgentSessionStore
/// not. The value only takes effect when this store is created for the first time, because the
/// platform fixes it at creation.
/// </param>
/// <param name="loggerFactory">Creates the logger this store reports through.</param>
public FoundryAgentSessionStore(
Uri? endpoint = null,
TokenCredential? credential = null,
string storeName = DefaultStoreName,
int itemTtlSeconds = FoundryStateStore.DefaultItemTtlSeconds)
int itemTtlSeconds = FoundryStateStore.DefaultItemTtlSeconds,
ILoggerFactory? loggerFactory = null)
{
_ = Throw.IfNullOrWhitespace(storeName);

this.StoreName = storeName;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<FoundryAgentSessionStore>();
this._binding = new(cancellationToken => FoundryStateStore.GetOrCreateAsync(
storeName,
credential,
Expand All @@ -107,12 +113,17 @@ public FoundryAgentSessionStore(
/// </summary>
/// <param name="storeFactory">Resolves the bound state store on first use.</param>
/// <param name="storeName">The state-store name, for diagnostics.</param>
internal FoundryAgentSessionStore(Func<CancellationToken, Task<FoundryStateStore>> storeFactory, string storeName = DefaultStoreName)
/// <param name="loggerFactory">Creates the logger this store reports through.</param>
internal FoundryAgentSessionStore(
Func<CancellationToken, Task<FoundryStateStore>> storeFactory,
string storeName = DefaultStoreName,
ILoggerFactory? loggerFactory = null)
{
_ = Throw.IfNull(storeFactory);

this._binding = new(storeFactory);
this.StoreName = storeName;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<FoundryAgentSessionStore>();
}

/// <summary>Gets the state-store name that holds the sessions.</summary>
Expand Down Expand Up @@ -177,6 +188,40 @@ await store.SetItemAsync(
return await agent.DeserializeSessionAsync(element, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public override async ValueTask DeleteSessionAsync(
AIAgent agent,
AgentSessionStoreKey key,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(agent);
_ = Throw.IfNull(key);

string logicalKey = FoundryAgentSessionKeyEncoder.BuildLogicalKey(
FoundryHostingAgent.GetSessionStorageIdentity(agent),
key);
FoundryStateStore store = await this.GetStoreAsync(cancellationToken).ConfigureAwait(false);

try
{
await store.DeleteItemAsync(
FoundryAgentSessionKeyEncoder.BuildStorageKey(logicalKey),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (FoundryStorageNotFoundException ex)
{
// Nothing stored for this key, so deletion remains idempotent.
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug(
ex,
"Session '{SessionId}' was not found in the Foundry state store '{StoreName}'. Nothing to delete.",
key.SessionId,
this.StoreName);
}
}
}

/// <summary>
/// Resolves the bound state store, creating it on the platform the first time. See
/// <see cref="FoundryStateStoreBinding"/> for the caching and failure behaviour.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Foundry.Hosting;

Expand Down Expand Up @@ -65,6 +66,19 @@ public override async ValueTask SaveSessionAsync(
return await agent.DeserializeSessionAsync(existingSession, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public override ValueTask DeleteSessionAsync(
AIAgent agent,
AgentSessionStoreKey key,
CancellationToken cancellationToken = default)
Comment thread
marcominerva marked this conversation as resolved.
{
_ = Throw.IfNull(agent);
_ = Throw.IfNull(key);

this._sessions.TryRemove(GetKey(agent, key), out _);
return default;
}

private static (string AgentIdentity, AgentSessionStoreKey Key) GetKey(
AIAgent agent,
AgentSessionStoreKey key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.Storage.Blobs;
using Microsoft.Agents.AI.Hosting.AzureStorage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Hosting;
Expand Down Expand Up @@ -77,7 +78,11 @@ public static IHostedAgentBuilder WithAzureBlobSessionStore(
{
BlobContainerClient containerClient =
Throw.IfNull(createBlobContainerClient(serviceProvider, agentName));
return new AzureBlobAgentSessionStore(containerClient, agentName, options);
return new AzureBlobAgentSessionStore(
containerClient,
agentName,
options,
serviceProvider.GetService<ILoggerFactory>());
},
lifetime,
withIsolation);
Expand Down
Loading
Loading