Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b778a52
draft commit
TheovanKraay Oct 29, 2025
2208d39
Added Cosmos agent thread and tests
TheovanKraay Oct 30, 2025
67c65aa
revert unnecessary changes and fix tests
TheovanKraay Oct 31, 2025
d020e18
add multi-tenant support with hierarchical partition keys (and tests).
TheovanKraay Oct 31, 2025
9de2fe8
enhance transactional batch
TheovanKraay Oct 31, 2025
b80c59e
address review comments
TheovanKraay Oct 31, 2025
042db62
Address PR review comments from @westey-m
TheovanKraay Nov 7, 2025
405d0ca
Merge upstream/main - resolve slnx conflicts
TheovanKraay Nov 7, 2025
7201533
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay Nov 7, 2025
7672a76
use param validation helpers
TheovanKraay Nov 7, 2025
394b749
Replace useManagedIdentity boolean with TokenCredential parameter
TheovanKraay Nov 7, 2025
76f300d
Remove redundant suppressions and fix tests
TheovanKraay Nov 7, 2025
bbbc651
Rename project from Microsoft.Agents.AI.Abstractions.CosmosNoSql to M…
TheovanKraay Nov 7, 2025
2015a76
Refactor constructors to use chaining pattern
TheovanKraay Nov 7, 2025
bea1b47
Reorder deserialization constructor parameters for consistency
TheovanKraay Nov 7, 2025
721a0b0
Remove database/container IDs from serialized state
TheovanKraay Nov 7, 2025
c2aae5f
Remove auto-generation of MessageId
TheovanKraay Nov 7, 2025
c5ad371
Optimize AddMessagesAsync to avoid enumeration when possible
TheovanKraay Nov 7, 2025
ba719f1
Add MaxMessagesToRetrieve to limit context window
TheovanKraay Nov 7, 2025
ba431e5
Make Role nullable instead of defaulting
TheovanKraay Nov 7, 2025
aef9491
Merge branch 'main' into csharp-cosmosdb-store-implementations
TheovanKraay Nov 7, 2025
03e7621
Fix net472 build without rebasing 19 commits
TheovanKraay Nov 7, 2025
8d0fa99
Add Cosmos DB emulator to CI workflow
TheovanKraay Nov 8, 2025
823b59a
Fix Cosmos DB emulator tests: use Skip.If instead of Assert.Fail and …
TheovanKraay Nov 8, 2025
ecc3498
Replace Skip.If() with conditional return to fix compilation
TheovanKraay Nov 8, 2025
7a574c6
Use env var to skip Cosmos tests on non-Windows CI
TheovanKraay Nov 8, 2025
8f0a3ba
Add Xunit.SkippableFact package to properly skip Cosmos tests on Linux
TheovanKraay Nov 8, 2025
76ba62b
Change [Fact] to [SkippableFact] for proper test skipping behavior
TheovanKraay Nov 8, 2025
4862ca4
Remove stale Microsoft.Agents.AI.Abstractions.CosmosNoSql directory
TheovanKraay Nov 8, 2025
b5675d6
Fix code formatting: add braces, this. qualifications, and final newl…
TheovanKraay Nov 8, 2025
e8c0e27
Fix file encoding to UTF-8 with BOM, fix import ordering, and remove …
TheovanKraay Nov 8, 2025
ec0a454
Convert backing fields to auto-properties and remove Azure.Identity u…
TheovanKraay Nov 8, 2025
0b49065
Fix CosmosChatMessageStore.cs encoding back to UTF-8 with BOM
TheovanKraay Nov 8, 2025
e6f4b79
Fix test file formatting: indentation, encoding, imports, this. quali…
TheovanKraay Nov 8, 2025
1d32377
Fix const field naming violations: Remove s_ prefix from const fields…
TheovanKraay Nov 8, 2025
b40a8ce
Add local .editorconfig for Cosmos DB tests to suppress IDE0005 false…
TheovanKraay Nov 8, 2025
10aecaa
Fix IDE1006 naming violations: Rename TestDatabaseId to s_testDatabas…
TheovanKraay Nov 8, 2025
d3200f7
Address PR review comments
TheovanKraay Nov 11, 2025
59b9cf8
Fix IDE0001 formatting error in AgentProviderExtensions.cs. Use type …
TheovanKraay Nov 11, 2025
9c736b5
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay Nov 18, 2025
108d7ab
Update package versions for Aspire 13.0.0 compatibility
TheovanKraay Nov 18, 2025
4c1a6b1
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay Nov 24, 2025
d472ad4
Fix TargetFrameworks in Cosmos DB projects
TheovanKraay Nov 24, 2025
5f06177
Remove redundant counter, add partition key validation, use factory p…
TheovanKraay Nov 25, 2025
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
17 changes: 17 additions & 0 deletions .github/workflows/dotnet-build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ jobs:
popd
rm -rf "$TEMP_DIR"

# Start Cosmos DB Emulator for Cosmos-based unit tests (only on Windows)
- name: Start Azure Cosmos DB Emulator
if: runner.os == 'Windows'
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV

- name: Run Unit Tests
shell: bash
run: |
Expand All @@ -143,6 +153,10 @@ jobs:
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
fi
done
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==

- name: Log event name and matrix integration-tests
shell: bash
Expand Down Expand Up @@ -181,6 +195,9 @@ jobs:
fi
done
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
# OpenAI Models
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
Expand Down
5 changes: 5 additions & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
Expand Down Expand Up @@ -127,6 +131,7 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xretry" Version="1.9.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<!-- Symbols -->
Expand Down
2 changes: 2 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@
<Folder Name="/src/">
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
Expand Down Expand Up @@ -372,6 +373,7 @@
<Folder Name="/Tests/UnitTests/">
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
Expand Down
688 changes: 688 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatMessageStore.cs

Large diffs are not rendered by default.

279 changes: 279 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosCheckpointStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Azure.Cosmos;
using Microsoft.Shared.Diagnostics;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Agents.AI.Workflows.Checkpointing;

/// <summary>
/// Provides a Cosmos DB implementation of the <see cref="JsonCheckpointStore"/> abstract class.
/// </summary>
/// <typeparam name="T">The type of objects to store as checkpoint values.</typeparam>
[RequiresUnreferencedCode("The CosmosCheckpointStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosCheckpointStore uses JSON serialization which is incompatible with NativeAOT.")]
public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
{
private readonly CosmosClient _cosmosClient;
private readonly Container _container;
private readonly bool _ownsClient;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="CosmosCheckpointStore{T}"/> class using a connection string.
/// </summary>
/// <param name="connectionString">The Cosmos DB connection string.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string connectionString, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions();

this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(connectionString), cosmosClientOptions);
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
this._ownsClient = true;
}

/// <summary>
/// Initializes a new instance of the <see cref="CosmosCheckpointStore{T}"/> class using a TokenCredential for authentication.
/// </summary>
/// <param name="accountEndpoint">The Cosmos DB account endpoint URI.</param>
/// <param name="tokenCredential">The TokenCredential to use for authentication (e.g., DefaultAzureCredential, ManagedIdentityCredential).</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
};

this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), cosmosClientOptions);
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
this._ownsClient = true;
}

/// <summary>
/// Initializes a new instance of the <see cref="CosmosCheckpointStore{T}"/> class using an existing <see cref="CosmosClient"/>.
/// </summary>
/// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param>
/// <param name="databaseId">The identifier of the Cosmos DB database.</param>
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(CosmosClient cosmosClient, string databaseId, string containerId)
{
this._cosmosClient = Throw.IfNull(cosmosClient);

this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
this._ownsClient = false;
}

/// <summary>
/// Gets the identifier of the Cosmos DB database.
/// </summary>
public string DatabaseId => this._container.Database.Id;

/// <summary>
/// Gets the identifier of the Cosmos DB container.
/// </summary>
public string ContainerId => this._container.Id;

/// <inheritdoc />
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string runId, JsonElement value, CheckpointInfo? parent = null)
{
if (string.IsNullOrWhiteSpace(runId))
{
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
}

#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
if (this._disposed)
{
throw new ObjectDisposedException(this.GetType().FullName);
}
#pragma warning restore CA1513

var checkpointId = Guid.NewGuid().ToString("N");
var checkpointInfo = new CheckpointInfo(runId, checkpointId);

var document = new CosmosCheckpointDocument
{
Id = $"{runId}_{checkpointId}",
RunId = runId,
CheckpointId = checkpointId,
Value = JToken.Parse(value.GetRawText()),
ParentCheckpointId = parent?.CheckpointId,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};

await this._container.CreateItemAsync(document, new PartitionKey(runId)).ConfigureAwait(false);
return checkpointInfo;
}

/// <inheritdoc />
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string runId, CheckpointInfo key)
{
if (string.IsNullOrWhiteSpace(runId))
{
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
}

if (key is null)
{
throw new ArgumentNullException(nameof(key));
}

#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
if (this._disposed)
{
throw new ObjectDisposedException(this.GetType().FullName);
}
#pragma warning restore CA1513

var id = $"{runId}_{key.CheckpointId}";

try
{
var response = await this._container.ReadItemAsync<CosmosCheckpointDocument>(id, new PartitionKey(runId)).ConfigureAwait(false);
using var document = JsonDocument.Parse(response.Resource.Value.ToString());
return document.RootElement.Clone();
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
throw new InvalidOperationException($"Checkpoint with ID '{key.CheckpointId}' for run '{runId}' not found.");
}
}

/// <inheritdoc />
public override async ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
{
if (string.IsNullOrWhiteSpace(runId))
{
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
}

#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
if (this._disposed)
{
throw new ObjectDisposedException(this.GetType().FullName);
}
#pragma warning restore CA1513

QueryDefinition query = withParent == null
? new QueryDefinition("SELECT c.runId, c.checkpointId FROM c WHERE c.runId = @runId ORDER BY c.timestamp ASC")
.WithParameter("@runId", runId)
: new QueryDefinition("SELECT c.runId, c.checkpointId FROM c WHERE c.runId = @runId AND c.parentCheckpointId = @parentCheckpointId ORDER BY c.timestamp ASC")
.WithParameter("@runId", runId)
.WithParameter("@parentCheckpointId", withParent.CheckpointId);

var iterator = this._container.GetItemQueryIterator<CheckpointQueryResult>(query);
var checkpoints = new List<CheckpointInfo>();

while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync().ConfigureAwait(false);
checkpoints.AddRange(response.Select(r => new CheckpointInfo(r.RunId, r.CheckpointId)));
}

return checkpoints;
}

/// <inheritdoc />
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases the unmanaged resources used by the <see cref="CosmosCheckpointStore{T}"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing && this._ownsClient)
{
this._cosmosClient?.Dispose();
}
this._disposed = true;
}
}

/// <summary>
/// Represents a checkpoint document stored in Cosmos DB.
/// </summary>
internal sealed class CosmosCheckpointDocument
{
[JsonProperty("id")]
public string Id { get; set; } = string.Empty;

[JsonProperty("runId")]
public string RunId { get; set; } = string.Empty;

[JsonProperty("checkpointId")]
public string CheckpointId { get; set; } = string.Empty;

[JsonProperty("value")]
public JToken Value { get; set; } = JValue.CreateNull();

[JsonProperty("parentCheckpointId")]
public string? ParentCheckpointId { get; set; }

[JsonProperty("timestamp")]
public long Timestamp { get; set; }
}

/// <summary>
/// Represents the result of a checkpoint query.
/// </summary>
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Cosmos DB query deserialization")]
private sealed class CheckpointQueryResult
{
public string RunId { get; set; } = string.Empty;
public string CheckpointId { get; set; } = string.Empty;
}
}

/// <summary>
/// Provides a non-generic Cosmos DB implementation of the <see cref="JsonCheckpointStore"/> abstract class.
/// </summary>
[RequiresUnreferencedCode("The CosmosCheckpointStore uses JSON serialization which is incompatible with trimming.")]
[RequiresDynamicCode("The CosmosCheckpointStore uses JSON serialization which is incompatible with NativeAOT.")]
public sealed class CosmosCheckpointStore : CosmosCheckpointStore<JsonElement>
{
/// <inheritdoc />
public CosmosCheckpointStore(string connectionString, string databaseId, string containerId)
: base(connectionString, databaseId, containerId)
{
}

/// <inheritdoc />
public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
: base(accountEndpoint, tokenCredential, databaseId, containerId)
{
}

/// <inheritdoc />
public CosmosCheckpointStore(CosmosClient cosmosClient, string databaseId, string containerId)
: base(cosmosClient, databaseId, containerId)
{
}
}
Loading
Loading