diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml
index 58d7c877..3d692e2a 100644
--- a/.github/workflows/pr_ci.yml
+++ b/.github/workflows/pr_ci.yml
@@ -47,4 +47,10 @@ jobs:
dotnet build -c Release -warnaserror /p:TreatWarningsAsErrors=true /p:RunAnalyzers=true /p:NuGetAudit=false
- name: Unit Tests
run: |
- dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj
+ dotnet test -c Release --no-build ./tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj
+ - name: Print Test Failure Details
+ if: failure()
+ shell: pwsh
+ run: |
+ Get-ChildItem ./tests/CrestApps.Core.Tests/bin/Release/net10.0/TestResults -Filter *.log -Recurse |
+ ForEach-Object { Get-Content $_.FullName }
diff --git a/CrestApps.Core.slnx b/CrestApps.Core.slnx
index 955f7402..92494fba 100644
--- a/CrestApps.Core.slnx
+++ b/CrestApps.Core.slnx
@@ -10,6 +10,10 @@
+
+
+
+
diff --git a/benchmarks/CrestApps.Core.Benchmarks/A2AAgentProxyToolBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/A2AAgentProxyToolBenchmarks.cs
new file mode 100644
index 00000000..d912087e
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/A2AAgentProxyToolBenchmarks.cs
@@ -0,0 +1,117 @@
+using System.Text.Json;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.A2A.Services;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures A2A proxy tool construction with per-instance versus shared JSON schema parsing.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class A2AAgentProxyToolConstructionBenchmarks
+{
+ private const string _jsonSchema =
+ """
+ {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "The message or task to send to the remote agent for processing."
+ },
+ "contextId": {
+ "type": "string",
+ "description": "An optional context identifier to maintain conversation continuity with the remote agent."
+ }
+ },
+ "required": ["message"]
+ }
+
+ """;
+
+ private int _schemaKindSink;
+
+ ///
+ /// Gets or sets the number of proxy tools created per benchmark operation.
+ ///
+ [Params(1, 100)]
+ public int ToolCount { get; set; }
+
+ ///
+ /// Verifies that the captured schema and production schema remain identical.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var legacySchema = JsonElement.Parse(_jsonSchema);
+ var tool = CreateTool(ToolCount);
+
+ if (!string.Equals(
+ legacySchema.GetRawText(),
+ tool.JsonSchema.GetRawText(),
+ StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Legacy and current A2A proxy schemas differ.");
+ }
+ }
+
+ ///
+ /// Creates proxy tools while parsing the original per-instance schema.
+ ///
+ /// The last created proxy tool.
+ [Benchmark(Baseline = true)]
+ public object CreateToolsLegacy()
+ {
+ A2AAgentProxyTool tool = null;
+ var schemaKind = 0;
+
+ for (var i = 0; i < ToolCount; i++)
+ {
+ var schema = JsonElement.Parse(_jsonSchema);
+ tool = CreateTool(i);
+ schemaKind += (int)schema.ValueKind;
+ }
+
+ _schemaKindSink = schemaKind;
+
+ return tool;
+ }
+
+ ///
+ /// Creates proxy tools with the shared production schema.
+ ///
+ /// The last created proxy tool.
+ [Benchmark]
+ public object CreateToolsCurrent()
+ {
+ A2AAgentProxyTool tool = null;
+ var schemaKind = 0;
+
+ for (var i = 0; i < ToolCount; i++)
+ {
+ tool = CreateTool(i);
+ schemaKind += (int)tool.JsonSchema.ValueKind;
+ }
+
+ _schemaKindSink = schemaKind;
+
+ return tool;
+ }
+
+ ///
+ /// Creates an A2A proxy tool.
+ ///
+ /// The tool index.
+ /// The proxy tool.
+ private static A2AAgentProxyTool CreateTool(int index)
+ {
+ return new A2AAgentProxyTool(
+ $"agent-{index}",
+ "Handles a benchmark task.",
+ "https://agent.example",
+ $"connection-{index}");
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/A2AToolRegistryProviderBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/A2AToolRegistryProviderBenchmarks.cs
new file mode 100644
index 00000000..d8165f4e
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/A2AToolRegistryProviderBenchmarks.cs
@@ -0,0 +1,343 @@
+using A2A;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.A2A.Models;
+using CrestApps.Core.AI.A2A.Services;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.Models;
+using CrestApps.Core.Services;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures A2A tool registry construction with in-memory connections and agent cards.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class A2AToolRegistryProviderBenchmarks
+{
+ private AICompletionContext _context;
+ private BenchmarkAgentCardCache _agentCardCache;
+ private BenchmarkConnectionCatalog _connectionCatalog;
+ private A2AToolRegistryProvider _provider;
+
+ ///
+ /// Gets or sets the number of A2A connections included in each registry request.
+ ///
+ [Params(100, 1000)]
+ public int ConnectionCount { get; set; }
+
+ ///
+ /// Gets or sets the percentage of skill names that require sanitization.
+ ///
+ [Params(0, 20)]
+ public int InvalidNamePercentage { get; set; }
+
+ ///
+ /// Creates in-memory connections and agent cards with ten skills per connection.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ const int skillsPerConnection = 10;
+
+ var connections = new Dictionary(ConnectionCount, StringComparer.Ordinal);
+ var cards = new Dictionary>(ConnectionCount, StringComparer.Ordinal);
+ var connectionIds = new string[ConnectionCount];
+
+ for (var connectionIndex = 0; connectionIndex < ConnectionCount; connectionIndex++)
+ {
+ var connectionId = $"connection_{connectionIndex}";
+ var skills = new List(skillsPerConnection);
+
+ for (var skillIndex = 0; skillIndex < skillsPerConnection; skillIndex++)
+ {
+ var ordinal = (connectionIndex * skillsPerConnection) + skillIndex;
+ var requiresSanitization = ordinal % 100 < InvalidNamePercentage;
+ skills.Add(new AgentSkill
+ {
+ Id = requiresSanitization
+ ? $"skill-{connectionIndex}-{skillIndex}"
+ : $"skill_{connectionIndex}_{skillIndex}",
+ Name = $"Skill {connectionIndex} {skillIndex}",
+ Description = $"Handles benchmark task {ordinal}.",
+ });
+ }
+
+ connectionIds[connectionIndex] = connectionId;
+ connections.Add(connectionId, new A2AConnection
+ {
+ ItemId = connectionId,
+ DisplayText = $"Connection {connectionIndex}",
+ Endpoint = $"https://agent-{connectionIndex}.example",
+ });
+ cards.Add(connectionId, Task.FromResult(new AgentCard
+ {
+ Name = $"Agent {connectionIndex}",
+ Description = $"Benchmark agent {connectionIndex}.",
+ Url = $"https://agent-{connectionIndex}.example",
+ Version = "1.0",
+ Skills = skills,
+ }));
+ }
+
+ _context = new AICompletionContext
+ {
+ A2AConnectionIds = connectionIds,
+ };
+ _connectionCatalog = new BenchmarkConnectionCatalog(connections);
+ _agentCardCache = new BenchmarkAgentCardCache(cards);
+ _provider = new A2AToolRegistryProvider(
+ _connectionCatalog,
+ _agentCardCache,
+ NullLogger.Instance);
+ }
+
+ ///
+ /// Builds registry entries with the original list and name-sanitization implementation.
+ ///
+ /// The registry entries.
+ [Benchmark(Baseline = true)]
+ public async Task> GetToolsLegacyAsync()
+ {
+ var connectionIds = _context?.A2AConnectionIds;
+
+ if (connectionIds is null || connectionIds.Length == 0)
+ {
+ return [];
+ }
+
+ var entries = new List();
+
+ foreach (var connectionId in connectionIds)
+ {
+ var connection = await _connectionCatalog.FindByIdAsync(connectionId);
+
+ if (connection is null || string.IsNullOrWhiteSpace(connection.Endpoint))
+ {
+ continue;
+ }
+
+ try
+ {
+ var agentCard = await _agentCardCache.GetAgentCardAsync(connectionId, connection);
+
+ if (agentCard?.Skills is null)
+ {
+ continue;
+ }
+
+ foreach (var skill in agentCard.Skills)
+ {
+ var skillName = SanitizeToolNameLegacy(skill.Id ?? skill.Name ?? connection.DisplayText);
+ var capturedConnectionId = connectionId;
+
+ entries.Add(new ToolRegistryEntry
+ {
+ Id = $"a2a:{connectionId}:{skillName}",
+ Name = skillName,
+ Description = skill.Description ?? agentCard.Description,
+ Source = ToolRegistryEntrySource.A2AAgent,
+ SourceId = connectionId,
+ CreateAsync = _ => ValueTask.FromResult(
+ new A2AAgentProxyTool(
+ skillName,
+ skill.Description ?? agentCard.Description,
+ connection.Endpoint,
+ capturedConnectionId)),
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ NullLogger.Instance.LogWarning(
+ ex,
+ "Failed to load agent card for A2A connection '{ConnectionId}'.",
+ connectionId);
+ }
+ }
+
+ return entries;
+ }
+
+ ///
+ /// Builds registry entries with the production implementation.
+ ///
+ /// The registry entries.
+ [Benchmark]
+ public Task> GetToolsOptimizedAsync()
+ {
+ return _provider.GetToolsAsync(_context);
+ }
+
+ ///
+ /// Sanitizes a tool name with the original allocation behavior.
+ ///
+ /// The tool name.
+ /// The sanitized tool name.
+ private static string SanitizeToolNameLegacy(string name)
+ {
+ var sanitized = new char[name.Length];
+
+ for (var i = 0; i < name.Length; i++)
+ {
+ var c = name[i];
+ sanitized[i] =
+ char.IsLetterOrDigit(c) || c == '_'
+ ? c
+ : '_';
+ }
+
+ return new string(sanitized);
+ }
+
+ private sealed class BenchmarkAgentCardCache : IA2AAgentCardCacheService
+ {
+ private readonly Dictionary> _cards;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agent cards keyed by connection identifier.
+ public BenchmarkAgentCardCache(Dictionary> cards)
+ {
+ _cards = cards;
+ }
+
+ ///
+ /// Gets a prebuilt agent card task.
+ ///
+ /// The connection identifier.
+ /// The connection.
+ /// The cancellation token.
+ /// The agent card task.
+ public Task GetAgentCardAsync(
+ string connectionId,
+ A2AConnection connection,
+ CancellationToken cancellationToken = default)
+ {
+ return _cards[connectionId];
+ }
+
+ ///
+ /// Performs no invalidation because benchmark cards are immutable.
+ ///
+ /// The connection identifier.
+ public void Invalidate(string connectionId)
+ {
+ }
+ }
+
+ private sealed class BenchmarkConnectionCatalog : ICatalog
+ {
+ private readonly Dictionary _connections;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The connections keyed by identifier.
+ public BenchmarkConnectionCatalog(Dictionary connections)
+ {
+ _connections = connections;
+ }
+
+ ///
+ /// Finds a connection by identifier.
+ ///
+ /// The connection identifier.
+ /// The cancellation token.
+ /// The connection when found.
+ public ValueTask FindByIdAsync(
+ string id,
+ CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult(_connections.GetValueOrDefault(id));
+ }
+
+ ///
+ /// Gets all benchmark connections.
+ ///
+ /// The cancellation token.
+ /// All connections.
+ public ValueTask> GetAllAsync(
+ CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult>(_connections.Values);
+ }
+
+ ///
+ /// Indicates that creating entries is unsupported.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ /// A value task that does not complete successfully.
+ public ValueTask CreateAsync(
+ A2AConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Indicates that deleting entries is unsupported.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ /// A value task that does not complete successfully.
+ public ValueTask DeleteAsync(
+ A2AConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Indicates that multi-entry lookup is unsupported.
+ ///
+ /// The entry identifiers.
+ /// The cancellation token.
+ /// A value task that does not complete successfully.
+ public ValueTask> GetAsync(
+ IEnumerable ids,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Indicates that paging is unsupported.
+ ///
+ /// The query context type.
+ /// The page number.
+ /// The page size.
+ /// The query context.
+ /// The cancellation token.
+ /// A value task that does not complete successfully.
+ public ValueTask> PageAsync(
+ int page,
+ int pageSize,
+ TQuery context,
+ CancellationToken cancellationToken = default)
+ where TQuery : QueryContext
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Indicates that updating entries is unsupported.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ /// A value task that does not complete successfully.
+ public ValueTask UpdateAsync(
+ A2AConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AIChatSessionExtractedDataIndexBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AIChatSessionExtractedDataIndexBenchmarks.cs
new file mode 100644
index 00000000..40ce5ad8
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AIChatSessionExtractedDataIndexBenchmarks.cs
@@ -0,0 +1,145 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.Data.YesSql.Indexes.AIChat;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures the extracted-data index mapping's legacy double sort against one shared ordered pass.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class AIChatSessionExtractedDataIndexBenchmarks
+{
+ private AIChatSessionExtractedDataRecord _record;
+
+ ///
+ /// Gets or sets the number of extracted fields.
+ ///
+ [Params(10, 100, 1000)]
+ public int FieldCount { get; set; }
+
+ ///
+ /// Creates fields with multiple values, duplicate values, and case-only key variants.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var values = new Dictionary>(FieldCount, StringComparer.Ordinal);
+
+ for (var index = FieldCount - 1; index >= 0; index--)
+ {
+ var pairIndex = index / 2;
+ var name = index % 2 == 0
+ ? $"field-{pairIndex:D4}"
+ : $"FIELD-{pairIndex:D4}";
+
+ values.Add(
+ name,
+ [
+ $"value-{index:D4}",
+ string.Empty,
+ $"VALUE-{index:D4}",
+ $"value-{index:D4}",
+ ]);
+ }
+
+ _record = new AIChatSessionExtractedDataRecord
+ {
+ SessionId = "session-1",
+ ProfileId = "profile-1",
+ SessionStartedUtc = new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc),
+ SessionEndedUtc = new DateTime(2026, 5, 1, 12, 5, 0, DateTimeKind.Utc),
+ Values = values,
+ UpdatedUtc = new DateTime(2026, 5, 1, 12, 6, 0, DateTimeKind.Utc),
+ };
+
+ EnsureEquivalent(MapWithDoubleSort(), MapWithSingleSort());
+ }
+
+ ///
+ /// Maps the record using the production implementation captured before the experiment.
+ ///
+ /// The mapped index.
+ [Benchmark(Baseline = true)]
+ public AIChatSessionExtractedDataIndex MapWithDoubleSort()
+ {
+ var fieldNames = _record.Values.Keys
+ .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ var valuesText = string.Join(
+ '\n',
+ _record.Values
+ .OrderBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase)
+ .SelectMany(pair => pair.Value.Select(value => $"{pair.Key}:{value}")));
+
+ return CreateIndex(fieldNames, valuesText);
+ }
+
+ ///
+ /// Maps the record by sorting field names once and sharing that order between outputs.
+ ///
+ /// The mapped index.
+ [Benchmark]
+ public AIChatSessionExtractedDataIndex MapWithSingleSort()
+ {
+ var fieldNames = _record.Values.Keys
+ .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ var valuesText = string.Join(
+ '\n',
+ fieldNames.SelectMany(
+ name => _record.Values[name].Select(value => $"{name}:{value}")));
+
+ return CreateIndex(fieldNames, valuesText);
+ }
+
+ ///
+ /// Creates an index using the mapped field-name and value text sequences.
+ ///
+ /// The ordered field names.
+ /// The flattened field values.
+ /// The mapped index.
+ private AIChatSessionExtractedDataIndex CreateIndex(
+ IEnumerable fieldNames,
+ string valuesText)
+ {
+ return new AIChatSessionExtractedDataIndex
+ {
+ SessionId = _record.SessionId,
+ ProfileId = _record.ProfileId,
+ SessionStartedUtc = _record.SessionStartedUtc,
+ SessionEndedUtc = _record.SessionEndedUtc,
+ FieldCount = _record.Values.Count,
+ FieldNames = string.Join('|', fieldNames),
+ ValuesText = valuesText,
+ UpdatedUtc = _record.UpdatedUtc,
+ };
+ }
+
+ ///
+ /// Verifies that both mapping implementations produce identical index values.
+ ///
+ /// The legacy mapping result.
+ /// The single-sort mapping result.
+ private static void EnsureEquivalent(
+ AIChatSessionExtractedDataIndex legacy,
+ AIChatSessionExtractedDataIndex candidate)
+ {
+ if (legacy.SessionId != candidate.SessionId ||
+ legacy.ProfileId != candidate.ProfileId ||
+ legacy.SessionStartedUtc != candidate.SessionStartedUtc ||
+ legacy.SessionEndedUtc != candidate.SessionEndedUtc ||
+ legacy.FieldCount != candidate.FieldCount ||
+ legacy.FieldNames != candidate.FieldNames ||
+ legacy.ValuesText != candidate.ValuesText ||
+ legacy.UpdatedUtc != candidate.UpdatedUtc)
+ {
+ throw new InvalidOperationException("Index mapping implementations returned different values.");
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AIDataSourceIndexingQueueNormalizationBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AIDataSourceIndexingQueueNormalizationBenchmarks.cs
new file mode 100644
index 00000000..8ebc5ad4
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AIDataSourceIndexingQueueNormalizationBenchmarks.cs
@@ -0,0 +1,357 @@
+using System.Threading.Channels;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Defines the document identifier distributions used by the indexing queue benchmarks.
+///
+public enum AIDataSourceDocumentIdDistribution
+{
+ ///
+ /// Every identifier is valid and unique.
+ ///
+ Unique,
+
+ ///
+ /// Half of the identifiers repeat an earlier identifier with identical casing.
+ ///
+ HalfDuplicates,
+
+ ///
+ /// Ninety percent of the identifiers are null, empty, or whitespace.
+ ///
+ MostlyInvalid,
+
+ ///
+ /// Every second identifier repeats the preceding identifier with different casing.
+ ///
+ CaseOnlyDuplicates,
+}
+
+///
+/// Measures the current queue normalization pipeline against a simple one-pass candidate.
+/// Each path writes one constructed work item to an available in-memory channel and performs
+/// no store, consumer, or network work. This class must remain unsealed because BenchmarkDotNet
+/// generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[InvocationCount(InvocationsPerIteration)]
+public class AIDataSourceIndexingQueueNormalizationBenchmarks
+{
+ private const int InvocationsPerIteration = 64;
+ private const string SourceIndexProfileName = "benchmark-source";
+ private OnePassAIDataSourceIndexingQueue _candidateQueue;
+ private IAsyncEnumerator _candidateReader;
+ private AIDataSourceIndexingQueue _currentQueue;
+ private IAsyncEnumerator _currentReader;
+ private string[] _documentIds;
+
+ ///
+ /// Gets or sets the number of input document identifiers.
+ ///
+ [Params(10, 100, 1_000, 10_000)]
+ public int IdCount { get; set; }
+
+ ///
+ /// Gets or sets the document identifier distribution.
+ ///
+ [Params(
+ AIDataSourceDocumentIdDistribution.Unique,
+ AIDataSourceDocumentIdDistribution.HalfDuplicates,
+ AIDataSourceDocumentIdDistribution.MostlyInvalid,
+ AIDataSourceDocumentIdDistribution.CaseOnlyDuplicates)]
+ public AIDataSourceDocumentIdDistribution Distribution { get; set; }
+
+ ///
+ /// Creates immutable inputs, independent channels, and verifies candidate equivalence.
+ ///
+ /// A task that completes after equivalence verification.
+ [GlobalSetup]
+ public async Task Setup()
+ {
+ _documentIds = CreateDocumentIds();
+ _currentQueue = new AIDataSourceIndexingQueue(NullLogger.Instance);
+ _candidateQueue = new OnePassAIDataSourceIndexingQueue(NullLogger.Instance);
+ _currentReader = _currentQueue.ReadAllAsync().GetAsyncEnumerator();
+ _candidateReader = _candidateQueue.ReadAllAsync().GetAsyncEnumerator();
+
+ await _currentQueue.QueueSyncSourceDocumentsAsync(SourceIndexProfileName, _documentIds);
+ await _candidateQueue.QueueSyncSourceDocumentsAsync(SourceIndexProfileName, _documentIds);
+
+ EnsureEquivalent(
+ await ReadNextAsync(_currentReader),
+ await ReadNextAsync(_candidateReader));
+ }
+
+ ///
+ /// Drains work items outside benchmark measurements so every write remains immediately available.
+ ///
+ [IterationCleanup]
+ public async Task Cleanup()
+ {
+ for (var index = 0; index < InvocationsPerIteration; index++)
+ {
+ await ReadNextAsync(_currentReader);
+ await ReadNextAsync(_candidateReader);
+ }
+ }
+
+ ///
+ /// Releases the queue readers after all benchmark cases complete.
+ ///
+ [GlobalCleanup]
+ public async Task DisposeReaders()
+ {
+ await _currentReader.DisposeAsync();
+ await _candidateReader.DisposeAsync();
+ }
+
+ ///
+ /// Enqueues with the current production Where, Distinct, and ToArray pipeline.
+ ///
+ /// A value task that completes when the in-memory channel accepts the item.
+ [Benchmark(Baseline = true)]
+ public ValueTask EnqueueWithCurrentLinq()
+ {
+ return _currentQueue.QueueSyncSourceDocumentsAsync(SourceIndexProfileName, _documentIds);
+ }
+
+ ///
+ /// Enqueues with a one-pass hash-set candidate that directly indexes array inputs.
+ ///
+ /// A value task that completes when the in-memory channel accepts the item.
+ [Benchmark]
+ public ValueTask EnqueueWithOnePassCandidate()
+ {
+ return _candidateQueue.QueueSyncSourceDocumentsAsync(SourceIndexProfileName, _documentIds);
+ }
+
+ ///
+ /// Creates the requested document identifier distribution.
+ ///
+ /// The generated identifiers.
+ private string[] CreateDocumentIds()
+ {
+ var documentIds = new string[IdCount];
+
+ for (var index = 0; index < documentIds.Length; index++)
+ {
+ documentIds[index] = Distribution switch
+ {
+ AIDataSourceDocumentIdDistribution.Unique => $"document-{index:D5}",
+ AIDataSourceDocumentIdDistribution.HalfDuplicates => $"document-{index % (IdCount / 2):D5}",
+ AIDataSourceDocumentIdDistribution.MostlyInvalid => CreateMostlyInvalidId(index),
+ AIDataSourceDocumentIdDistribution.CaseOnlyDuplicates => index % 2 == 0
+ ? $"Document-{index / 2:D5}"
+ : $"DOCUMENT-{index / 2:D5}",
+ _ => throw new InvalidOperationException($"Unsupported distribution '{Distribution}'."),
+ };
+ }
+
+ return documentIds;
+ }
+
+ ///
+ /// Creates one valid identifier followed by nine null, empty, or whitespace values.
+ ///
+ /// The source position.
+ /// The identifier at the requested position.
+ private static string CreateMostlyInvalidId(int index)
+ {
+ return (index % 10) switch
+ {
+ 0 => $"document-{index:D5}",
+ 1 => null,
+ 2 => string.Empty,
+ 3 => " ",
+ 4 => "\t",
+ 5 => "\r\n",
+ 6 => "\u00a0",
+ 7 => " \t ",
+ 8 => "\n",
+ _ => "\r",
+ };
+ }
+
+ ///
+ /// Verifies normalized identifiers, target, and work-item operation remain exactly equivalent.
+ ///
+ /// The current production work item.
+ /// The candidate work item.
+ private static void EnsureEquivalent(
+ AIDataSourceIndexingWorkItem current,
+ AIDataSourceIndexingWorkItem candidate)
+ {
+ if (current.Type != candidate.Type ||
+ !string.Equals(current.SourceIndexProfileName, candidate.SourceIndexProfileName, StringComparison.Ordinal) ||
+ !current.DocumentIds.SequenceEqual(candidate.DocumentIds, StringComparer.Ordinal))
+ {
+ throw new InvalidOperationException("Queue normalization candidates produced different work items.");
+ }
+ }
+
+ ///
+ /// Reads the next queued work item outside benchmark measurements.
+ ///
+ /// The queue reader.
+ /// The queued work item.
+ private static async ValueTask ReadNextAsync(
+ IAsyncEnumerator reader)
+ {
+ if (!await reader.MoveNextAsync())
+ {
+ throw new InvalidOperationException("Expected a queued work item.");
+ }
+
+ return reader.Current;
+ }
+
+ ///
+ /// Captures the rejected one-pass implementation with the production queue structure.
+ ///
+ private sealed class OnePassAIDataSourceIndexingQueue
+ {
+ private readonly Channel _channel = Channel.CreateUnbounded(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new candidate queue.
+ ///
+ /// The logger.
+ public OnePassAIDataSourceIndexingQueue(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Queues source documents with the rejected one-pass normalization candidate.
+ ///
+ /// The source index profile name.
+ /// The source document identifiers.
+ /// The cancellation token.
+ /// A value task that completes when the channel accepts the item.
+ public ValueTask QueueSyncSourceDocumentsAsync(
+ string sourceIndexProfileName,
+ IReadOnlyCollection documentIds,
+ CancellationToken cancellationToken = default)
+ {
+ return QueueDocumentIdsAsync(
+ sourceIndexProfileName,
+ nameof(sourceIndexProfileName),
+ documentIds,
+ AIDataSourceIndexingWorkItem.ForSyncSourceDocuments,
+ cancellationToken);
+ }
+
+ ///
+ /// Normalizes document identifiers and queues one targeted work item.
+ ///
+ /// The source index profile name or data source identifier.
+ /// The public target parameter name used for validation failures.
+ /// The source document identifiers.
+ /// The work-item factory for the requested operation.
+ /// The cancellation token.
+ /// A value task that completes when the work item is accepted by the channel.
+ private ValueTask QueueDocumentIdsAsync(
+ string target,
+ string targetParameterName,
+ IReadOnlyCollection documentIds,
+ Func, AIDataSourceIndexingWorkItem> factory,
+ CancellationToken cancellationToken)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(target, targetParameterName);
+ ArgumentNullException.ThrowIfNull(documentIds);
+
+ var ids = Normalize(documentIds);
+
+ if (ids.Length == 0)
+ {
+ if (_logger.IsEnabled(LogLevel.Trace))
+ {
+ _logger.LogTrace("Skipped queueing data-source work item because no document ids remained after normalization.");
+ }
+
+ return ValueTask.CompletedTask;
+ }
+
+ var workItem = factory(target, ids);
+
+ LogQueuedWorkItem(workItem, target, ids.Length);
+
+ return _channel.Writer.WriteAsync(workItem, cancellationToken);
+ }
+
+ ///
+ /// Normalizes identifiers with one direct source pass and case-insensitive set semantics.
+ ///
+ /// The source document identifiers.
+ /// The normalized identifier array.
+ private static string[] Normalize(IReadOnlyCollection documentIds)
+ {
+ var ids = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ if (documentIds is string[] sourceIds)
+ {
+ for (var index = 0; index < sourceIds.Length; index++)
+ {
+ var documentId = sourceIds[index];
+
+ if (!string.IsNullOrWhiteSpace(documentId))
+ {
+ ids.Add(documentId);
+ }
+ }
+
+ return [.. ids];
+ }
+
+ foreach (var documentId in documentIds)
+ {
+ if (!string.IsNullOrWhiteSpace(documentId))
+ {
+ ids.Add(documentId);
+ }
+ }
+
+ return [.. ids];
+ }
+
+ ///
+ /// Logs a queued candidate work item when trace logging is enabled.
+ ///
+ /// The queued work item.
+ /// The work-item target.
+ /// The normalized document count.
+ private void LogQueuedWorkItem(
+ AIDataSourceIndexingWorkItem workItem,
+ string target,
+ int documentCount)
+ {
+ if (_logger.IsEnabled(LogLevel.Trace))
+ {
+ _logger.LogTrace("Queued data-source work item {WorkItemType}. Target={Target}, DocumentCount={DocumentCount}.", workItem.Type, target, documentCount);
+ }
+ }
+
+ ///
+ /// Reads queued work items.
+ ///
+ /// The cancellation token.
+ /// The queued work items.
+ public IAsyncEnumerable ReadAllAsync(
+ CancellationToken cancellationToken = default)
+ {
+ return _channel.Reader.ReadAllAsync(cancellationToken);
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AIDocumentIndexingMaterializationBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AIDocumentIndexingMaterializationBenchmarks.cs
new file mode 100644
index 00000000..03339e26
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AIDocumentIndexingMaterializationBenchmarks.cs
@@ -0,0 +1,231 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.Infrastructure;
+using CrestApps.Core.Infrastructure.Indexing.Models;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures local AI document chunk filtering and index-document materialization.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class AIDocumentIndexingMaterializationBenchmarks
+{
+ private AIDocumentChunk[] _chunks;
+ private AIDocument _document;
+
+ ///
+ /// Gets or sets the number of chunks supplied to the materializer.
+ ///
+ [Params(10, 100, 1000, 10000)]
+ public int ChunkCount { get; set; }
+
+ ///
+ /// Creates realistic immutable benchmark inputs and verifies candidate equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _document = new AIDocument
+ {
+ ItemId = "document-benchmark",
+ ReferenceId = "profile-benchmark",
+ ReferenceType = "profile",
+ FileName = "quarterly-research-report.pdf",
+ };
+ _chunks = new AIDocumentChunk[ChunkCount];
+
+ for (var i = 0; i < _chunks.Length; i++)
+ {
+ _chunks[i] = new AIDocumentChunk
+ {
+ ItemId = $"chunk-{i:D5}",
+ AIDocumentId = _document.ItemId,
+ ReferenceId = _document.ReferenceId,
+ ReferenceType = _document.ReferenceType,
+ Content = i % 20 == 0
+ ? " "
+ : $"Section {i}: revenue, customer activity, operational results, and cited source details.",
+ Embedding = i % 20 == 10
+ ? []
+ : [0.125f, 0.25f, 0.5f, 0.75f, 0.875f, 1f],
+ Index = i,
+ };
+ }
+
+ var legacy = MaterializeLegacyCore();
+ EnsureEquivalent(legacy, MaterializeDirectArrayCore());
+ EnsureEquivalent(legacy, MaterializeCountAwareCore());
+ }
+
+ ///
+ /// Materializes documents with the production implementation captured before optimization.
+ ///
+ /// The materialized index documents.
+ [Benchmark(Baseline = true)]
+ public IndexDocument[] MaterializeLegacy()
+ {
+ return MaterializeLegacyCore();
+ }
+
+ ///
+ /// Materializes documents by filling the known-size output array directly.
+ ///
+ /// The materialized index documents.
+ [Benchmark]
+ public IndexDocument[] MaterializeDirectArray()
+ {
+ return MaterializeDirectArrayCore();
+ }
+
+ ///
+ /// Materializes documents with a count-aware two-stage candidate.
+ ///
+ /// The materialized index documents.
+ [Benchmark]
+ public IndexDocument[] MaterializeCountAware()
+ {
+ return MaterializeCountAwareCore();
+ }
+
+ private IndexDocument[] MaterializeLegacyCore()
+ {
+ var indexedChunks = _chunks
+ .Where(chunk => chunk.Embedding is { Length: > 0 } && !string.IsNullOrWhiteSpace(chunk.Content))
+ .ToList();
+
+ return indexedChunks
+ .Select(chunk => new IndexDocument
+ {
+ Id = chunk.ItemId,
+ Fields = new Dictionary
+ {
+ [DocumentIndexConstants.ColumnNames.ChunkId] = chunk.ItemId,
+ [DocumentIndexConstants.ColumnNames.DocumentId] = _document.ItemId,
+ [DocumentIndexConstants.ColumnNames.Content] = chunk.Content,
+ [DocumentIndexConstants.ColumnNames.FileName] = _document.FileName,
+ [DocumentIndexConstants.ColumnNames.ReferenceId] = chunk.ReferenceId,
+ [DocumentIndexConstants.ColumnNames.ReferenceType] = chunk.ReferenceType,
+ [DocumentIndexConstants.ColumnNames.Embedding] = chunk.Embedding,
+ [DocumentIndexConstants.ColumnNames.ChunkIndex] = chunk.Index,
+ },
+ })
+ .ToArray();
+ }
+
+ private IndexDocument[] MaterializeDirectArrayCore()
+ {
+ var indexedChunks = _chunks
+ .Where(chunk => chunk.Embedding is { Length: > 0 } && !string.IsNullOrWhiteSpace(chunk.Content))
+ .ToList();
+ var documents = new IndexDocument[indexedChunks.Count];
+
+ for (var i = 0; i < indexedChunks.Count; i++)
+ {
+ var chunk = indexedChunks[i];
+ documents[i] = new IndexDocument
+ {
+ Id = chunk.ItemId,
+ Fields = new Dictionary
+ {
+ [DocumentIndexConstants.ColumnNames.ChunkId] = chunk.ItemId,
+ [DocumentIndexConstants.ColumnNames.DocumentId] = _document.ItemId,
+ [DocumentIndexConstants.ColumnNames.Content] = chunk.Content,
+ [DocumentIndexConstants.ColumnNames.FileName] = _document.FileName,
+ [DocumentIndexConstants.ColumnNames.ReferenceId] = chunk.ReferenceId,
+ [DocumentIndexConstants.ColumnNames.ReferenceType] = chunk.ReferenceType,
+ [DocumentIndexConstants.ColumnNames.Embedding] = chunk.Embedding,
+ [DocumentIndexConstants.ColumnNames.ChunkIndex] = chunk.Index,
+ },
+ };
+ }
+
+ return documents;
+ }
+
+ private IndexDocument[] MaterializeCountAwareCore()
+ {
+ var indexedChunks = new AIDocumentChunk[_chunks.Length];
+ var indexedChunkCount = 0;
+
+ foreach (var chunk in _chunks)
+ {
+ if (chunk.Embedding is not { Length: > 0 } ||
+ string.IsNullOrWhiteSpace(chunk.Content))
+ {
+ continue;
+ }
+
+ indexedChunks[indexedChunkCount++] = chunk;
+ }
+
+ var documents = new IndexDocument[indexedChunkCount];
+
+ for (var i = 0; i < indexedChunkCount; i++)
+ {
+ var chunk = indexedChunks[i];
+ documents[i] = new IndexDocument
+ {
+ Id = chunk.ItemId,
+ Fields = new Dictionary
+ {
+ [DocumentIndexConstants.ColumnNames.ChunkId] = chunk.ItemId,
+ [DocumentIndexConstants.ColumnNames.DocumentId] = _document.ItemId,
+ [DocumentIndexConstants.ColumnNames.Content] = chunk.Content,
+ [DocumentIndexConstants.ColumnNames.FileName] = _document.FileName,
+ [DocumentIndexConstants.ColumnNames.ReferenceId] = chunk.ReferenceId,
+ [DocumentIndexConstants.ColumnNames.ReferenceType] = chunk.ReferenceType,
+ [DocumentIndexConstants.ColumnNames.Embedding] = chunk.Embedding,
+ [DocumentIndexConstants.ColumnNames.ChunkIndex] = chunk.Index,
+ },
+ };
+ }
+
+ return documents;
+ }
+
+ private static void EnsureEquivalent(
+ IndexDocument[] legacy,
+ IndexDocument[] candidate)
+ {
+ if (legacy.Length != candidate.Length)
+ {
+ throw new InvalidOperationException("Legacy and candidate document counts differ.");
+ }
+
+ for (var i = 0; i < legacy.Length; i++)
+ {
+ var legacyDocument = legacy[i];
+ var candidateDocument = candidate[i];
+
+ if (!string.Equals(legacyDocument.Id, candidateDocument.Id, StringComparison.Ordinal) ||
+ legacyDocument.Fields.Count != candidateDocument.Fields.Count)
+ {
+ throw new InvalidOperationException("Legacy and candidate documents differ.");
+ }
+
+ foreach (var (key, legacyValue) in legacyDocument.Fields)
+ {
+ if (!candidateDocument.Fields.TryGetValue(key, out var candidateValue) ||
+ !ValuesEqual(legacyValue, candidateValue))
+ {
+ throw new InvalidOperationException($"Legacy and candidate field '{key}' differs.");
+ }
+ }
+ }
+ }
+
+ private static bool ValuesEqual(object legacyValue, object candidateValue)
+ {
+ if (legacyValue is float[] legacyEmbedding &&
+ candidateValue is float[] candidateEmbedding)
+ {
+ return legacyEmbedding.SequenceEqual(candidateEmbedding);
+ }
+
+ return Equals(legacyValue, candidateValue);
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AIFunctionArgumentsBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AIFunctionArgumentsBenchmarks.cs
new file mode 100644
index 00000000..404d637c
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AIFunctionArgumentsBenchmarks.cs
@@ -0,0 +1,190 @@
+using System.Text.Json;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Extensions;
+using Microsoft.Extensions.AI;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures typed values read from JSON-backed AI function arguments.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+[CategoriesColumn]
+public class AIFunctionArgumentsBenchmarks
+{
+ private JsonDocument _document;
+ private AIFunctionArguments _arguments;
+
+ ///
+ /// Creates representative JSON-backed function arguments.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _document = JsonDocument.Parse("""
+ {
+ "number": 42,
+ "person": {
+ "name": "Ada",
+ "age": 37
+ },
+ "items": ["one", "two", "three", "four"]
+ }
+ """);
+
+ _arguments = new AIFunctionArguments
+ {
+ ["number"] = _document.RootElement.GetProperty("number"),
+ ["person"] = _document.RootElement.GetProperty("person"),
+ ["items"] = _document.RootElement.GetProperty("items"),
+ };
+ }
+
+ ///
+ /// Disposes the JSON document.
+ ///
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _document.Dispose();
+ }
+
+ ///
+ /// Reads an integer argument.
+ ///
+ /// The converted integer.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Int32")]
+ public int ReadInt32Buffered()
+ {
+ TryGetFirstLegacy(_arguments, "number", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Reads an integer argument without materializing raw JSON text.
+ ///
+ /// The converted integer.
+ [Benchmark]
+ [BenchmarkCategory("Int32")]
+ public int ReadInt32Optimized()
+ {
+ _arguments.TryGetFirst("number", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Reads an object argument.
+ ///
+ /// The converted object.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Object")]
+ public Person ReadObjectBuffered()
+ {
+ TryGetFirstLegacy(_arguments, "person", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Reads an object argument without materializing raw JSON text.
+ ///
+ /// The converted object.
+ [Benchmark]
+ [BenchmarkCategory("Object")]
+ public Person ReadObjectOptimized()
+ {
+ _arguments.TryGetFirst("person", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Reads an array argument.
+ ///
+ /// The converted array.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Array")]
+ public string[] ReadArrayBuffered()
+ {
+ TryGetFirstLegacy(_arguments, "items", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Reads an array argument without materializing raw JSON text.
+ ///
+ /// The converted array.
+ [Benchmark]
+ [BenchmarkCategory("Array")]
+ public string[] ReadArrayOptimized()
+ {
+ _arguments.TryGetFirst("items", out var value);
+
+ return value;
+ }
+
+ ///
+ /// Preserves the original raw-text JSON conversion as a benchmark baseline.
+ ///
+ /// The target value type.
+ /// The function arguments.
+ /// The argument key.
+ /// Receives the converted value.
+ /// when conversion succeeds.
+ private static bool TryGetFirstLegacy(
+ AIFunctionArguments arguments,
+ string key,
+ out T value)
+ {
+ value = default;
+
+ if (!arguments.TryGetValue(key, out var unsafeValue) || unsafeValue is null)
+ {
+ return false;
+ }
+
+ if (unsafeValue is T alreadyTyped)
+ {
+ value = alreadyTyped;
+
+ return true;
+ }
+
+ if (unsafeValue is JsonElement jsonElement)
+ {
+ value = JsonSerializer.Deserialize(
+ jsonElement.GetRawText(),
+ JSOptions.CaseInsensitive);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Represents a benchmark function argument.
+ ///
+ public sealed class Person
+ {
+ ///
+ /// Gets or sets the person's name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the person's age.
+ ///
+ public int Age { get; set; }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AudioChunkBase64EncodingBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AudioChunkBase64EncodingBenchmarks.cs
new file mode 100644
index 00000000..533ce4d6
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AudioChunkBase64EncodingBenchmarks.cs
@@ -0,0 +1,107 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the legacy copied audio-chunk Base64 conversion with span-based conversion.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class AudioChunkBase64EncodingBenchmarks
+{
+ private ReadOnlyMemory[] _chunks;
+
+ ///
+ /// Gets or sets the number of bytes in each audio chunk.
+ ///
+ [Params(256, 4 * 1024, 64 * 1024, 1024 * 1024)]
+ public int ChunkSize { get; set; }
+
+ ///
+ /// Gets or sets the offset of each chunk within its backing buffer.
+ ///
+ [Params(0, 17)]
+ public int SliceOffset { get; set; }
+
+ ///
+ /// Creates deterministic binary audio chunks and verifies exact output equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var chunkCount = GetChunkCount(ChunkSize);
+ var random = new Random(42);
+ _chunks = new ReadOnlyMemory[chunkCount];
+
+ for (var index = 0; index < _chunks.Length; index++)
+ {
+ var buffer = new byte[ChunkSize + (2 * SliceOffset)];
+ random.NextBytes(buffer);
+ var chunk = buffer.AsMemory(SliceOffset, ChunkSize);
+ chunk.Span[0] = 0;
+ chunk.Span[^1] = 255;
+ _chunks[index] = chunk;
+
+ var legacy = Convert.ToBase64String(chunk.ToArray());
+ var current = Convert.ToBase64String(chunk.Span);
+
+ if (!string.Equals(legacy, current, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Legacy and span-based Base64 conversion produced different output.");
+ }
+ }
+ }
+
+ ///
+ /// Encodes every chunk after copying its selected memory range to a new array.
+ ///
+ /// The total encoded character count.
+ [Benchmark(Baseline = true)]
+ public int ToArrayThenBase64()
+ {
+ var encodedLength = 0;
+
+ foreach (var audioData in _chunks)
+ {
+ encodedLength += Convert.ToBase64String(audioData.ToArray()).Length;
+ }
+
+ return encodedLength;
+ }
+
+ ///
+ /// Encodes every chunk directly from its selected memory span.
+ ///
+ /// The total encoded character count.
+ [Benchmark]
+ public int SpanBase64()
+ {
+ var encodedLength = 0;
+
+ foreach (var audioData in _chunks)
+ {
+ encodedLength += Convert.ToBase64String(audioData.Span).Length;
+ }
+
+ return encodedLength;
+ }
+
+ ///
+ /// Gets a representative streaming chunk count for the requested chunk size.
+ ///
+ /// The chunk size in bytes.
+ /// The number of chunks encoded per benchmark operation.
+ private static int GetChunkCount(int chunkSize)
+ {
+ return chunkSize switch
+ {
+ 256 => 256,
+ 4 * 1024 => 64,
+ 64 * 1024 => 16,
+ 1024 * 1024 => 4,
+ _ => throw new InvalidOperationException($"Unsupported chunk size '{chunkSize}'."),
+ };
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AzureAISearchAIDataSourceReadByIdsFilterBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AzureAISearchAIDataSourceReadByIdsFilterBenchmarks.cs
new file mode 100644
index 00000000..fa75588b
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AzureAISearchAIDataSourceReadByIdsFilterBenchmarks.cs
@@ -0,0 +1,67 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.Azure.AISearch.Services;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the legacy and current Azure AI Search AI data source ReadByIds OData filter
+/// construction. The identifiers represent the already whitespace-filtered and case-insensitively
+/// de-duplicated array that AzureAISearchAIDataSourceSourceHandler.ReadByIdsAsync passes to
+/// filter construction, so the measured region isolates the step that changed: replacing the
+/// per-identifier LINQ projection with the shared filter builder.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class AzureAISearchAIDataSourceReadByIdsFilterBenchmarks
+{
+ private const string KeyFieldName = "documentId";
+
+ private string[] _documentIds;
+
+ ///
+ /// Gets or sets the number of document identifiers included in each filter.
+ ///
+ [Params(1, 10, 100, 1000)]
+ public int DocumentIdCount { get; set; }
+
+ ///
+ /// Creates the prepared document identifiers, where every tenth identifier includes apostrophes
+ /// to exercise OData escaping.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _documentIds = new string[DocumentIdCount];
+
+ for (var index = 0; index < DocumentIdCount; index++)
+ {
+ _documentIds[index] = index % 10 == 0
+ ? $"document-'quoted'-{index}"
+ : $"document-{index}";
+ }
+ }
+
+ ///
+ /// Builds the OData filter with the original per-identifier LINQ projection implementation.
+ ///
+ /// The OData filter.
+ [Benchmark(Baseline = true)]
+ public string PrepareLegacy()
+ {
+ return string.Join(
+ " or ",
+ _documentIds.Select(id => $"{KeyFieldName} eq '{id.Replace("'", "''", StringComparison.Ordinal)}'"));
+ }
+
+ ///
+ /// Builds the OData filter with the shared builder implementation.
+ ///
+ /// The OData filter.
+ [Benchmark]
+ public string PrepareCurrent()
+ {
+ return DataSourceAzureAISearchDocumentIdFilterBuilder.BuildFilter(_documentIds, KeyFieldName);
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAICompletionClientHistoryBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAICompletionClientHistoryBenchmarks.cs
new file mode 100644
index 00000000..a0b05ae1
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAICompletionClientHistoryBenchmarks.cs
@@ -0,0 +1,450 @@
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.OpenAI.Azure.Services;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+using AIChatMessage = Microsoft.Extensions.AI.ChatMessage;
+using AIChatRole = Microsoft.Extensions.AI.ChatRole;
+using SdkChatMessage = OpenAI.Chat.ChatMessage;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured Azure adapter history conversion with the bounded production converter for
+/// dense text and sparse-eligibility histories. This class must remain unsealed because
+/// BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class AzureOpenAICompletionClientHistoryBenchmarks
+{
+ private AICompletionContext _context;
+ private List _messages;
+
+ ///
+ /// Gets or sets the total number of raw messages.
+ ///
+ [Params(10, 1_000, 10_000)]
+ public int MessageCount { get; set; }
+
+ ///
+ /// Gets or sets the number of converted history messages retained.
+ ///
+ [Params(10, 50)]
+ public int PastMessagesCount { get; set; }
+
+ ///
+ /// Gets or sets the raw-message distribution.
+ ///
+ [Params(AzureHistoryProfile.DenseText, AzureHistoryProfile.SparseEligibility)]
+ public AzureHistoryProfile Profile { get; set; }
+
+ ///
+ /// Creates stable inputs and verifies exact converted SDK message and content equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _messages = AzureOpenAIHistoryBenchmarkCases.CreateMessages(
+ MessageCount,
+ Profile,
+ includeImages: false);
+ _context = new AICompletionContext
+ {
+ PastMessagesCount = PastMessagesCount,
+ SystemMessage = "benchmark-system",
+ };
+
+ AzureOpenAIHistoryBenchmarkCases.EnsureEquivalent(
+ AzureOpenAIHistoryBenchmarkCases.Legacy(_messages, _context),
+ AzureOpenAIHistoryBenchmarkCases.Current(_messages, _context));
+ }
+
+ ///
+ /// Converts all eligible raw messages before selecting the configured tail.
+ ///
+ /// The converted prompts.
+ [Benchmark(Baseline = true)]
+ public List Legacy()
+ {
+ return AzureOpenAIHistoryBenchmarkCases.Legacy(_messages, _context);
+ }
+
+ ///
+ /// Classifies a bounded eligible tail before creating retained SDK messages.
+ ///
+ /// The converted prompts.
+ [Benchmark]
+ public List Current()
+ {
+ return AzureOpenAIHistoryBenchmarkCases.Current(_messages, _context);
+ }
+}
+
+///
+/// Compares image-heavy histories at reduced scales so each tenth eligible message can retain a
+/// realistic 256 KB image without making the short-run suite impractical. This class must remain
+/// unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class AzureOpenAICompletionClientImageHistoryBenchmarks
+{
+ private AICompletionContext _context;
+ private List _messages;
+
+ ///
+ /// Gets or sets the total number of raw messages.
+ ///
+ [Params(10, 100, 1_000)]
+ public int MessageCount { get; set; }
+
+ ///
+ /// Gets or sets the number of converted history messages retained.
+ ///
+ [Params(10, 50)]
+ public int PastMessagesCount { get; set; }
+
+ ///
+ /// Gets or sets the raw-message distribution.
+ ///
+ [Params(AzureHistoryProfile.DenseText, AzureHistoryProfile.SparseEligibility)]
+ public AzureHistoryProfile Profile { get; set; }
+
+ ///
+ /// Creates stable inputs and verifies exact converted SDK message, content, media type, and byte equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _messages = AzureOpenAIHistoryBenchmarkCases.CreateMessages(
+ MessageCount,
+ Profile,
+ includeImages: true);
+ _context = new AICompletionContext
+ {
+ PastMessagesCount = PastMessagesCount,
+ SystemMessage = "benchmark-system",
+ };
+
+ AzureOpenAIHistoryBenchmarkCases.EnsureEquivalent(
+ AzureOpenAIHistoryBenchmarkCases.Legacy(_messages, _context),
+ AzureOpenAIHistoryBenchmarkCases.Current(_messages, _context));
+ }
+
+ ///
+ /// Converts and copies every eligible image before selecting the configured tail.
+ ///
+ /// The converted prompts.
+ [Benchmark(Baseline = true)]
+ public List Legacy()
+ {
+ return AzureOpenAIHistoryBenchmarkCases.Legacy(_messages, _context);
+ }
+
+ ///
+ /// Copies image bytes at encounter time but creates eager SDK image data URIs only for retained messages.
+ ///
+ /// The converted prompts.
+ [Benchmark]
+ public List Current()
+ {
+ return AzureOpenAIHistoryBenchmarkCases.Current(_messages, _context);
+ }
+}
+
+///
+/// Defines the benchmark history distributions.
+///
+public enum AzureHistoryProfile
+{
+ ///
+ /// Every raw message is eligible.
+ ///
+ DenseText,
+
+ ///
+ /// One raw message in ten is eligible.
+ ///
+ SparseEligibility,
+}
+
+///
+/// Provides captured legacy and production bounded-tail benchmark cases.
+///
+internal static class AzureOpenAIHistoryBenchmarkCases
+{
+ private const int ImageSize = 256 * 1024;
+
+ private static readonly Func, List> _getPrompts =
+ CreateGetPromptsDelegate();
+
+ ///
+ /// Converts all eligible messages and then applies the production history tail.
+ ///
+ /// The raw messages.
+ /// The completion context.
+ /// The converted prompts.
+ public static List Legacy(
+ IEnumerable messages,
+ AICompletionContext context)
+ {
+ var converted = new List();
+ var currentPrompt = string.Empty;
+
+ foreach (var message in messages)
+ {
+ if (message.Role == AIChatRole.User)
+ {
+ var userMessage = CreateUserMessageLegacy(message);
+
+ if (userMessage != null)
+ {
+ converted.Add(userMessage);
+ currentPrompt = message.Text;
+ }
+ }
+ else if (message.Role == AIChatRole.Assistant && !string.IsNullOrWhiteSpace(message.Text))
+ {
+ converted.Add(new AssistantChatMessage(message.Text));
+ }
+ }
+
+ _ = currentPrompt;
+
+ return _getPrompts(context, converted);
+ }
+
+ ///
+ /// Converts messages with the production bounded converter and applies system-message construction.
+ ///
+ /// The raw messages.
+ /// The completion context.
+ /// The converted prompts.
+ public static List Current(
+ IEnumerable messages,
+ AICompletionContext context)
+ {
+ var converted = AzureOpenAIChatMessageConverter.Convert(
+ messages,
+ context.PastMessagesCount);
+
+ return _getPrompts(context, converted);
+ }
+
+ ///
+ /// Creates raw messages for the requested scale and distribution.
+ ///
+ /// The total raw-message count.
+ /// The raw-message distribution.
+ /// Whether every tenth eligible message should carry a 256 KB image.
+ /// The benchmark messages.
+ public static List CreateMessages(
+ int messageCount,
+ AzureHistoryProfile profile,
+ bool includeImages)
+ {
+ var messages = new List(messageCount);
+ var imageBytes = Enumerable
+ .Range(0, ImageSize)
+ .Select(index => (byte)(index % 251))
+ .ToArray();
+ var eligibleIndex = 0;
+
+ for (var index = 0; index < messageCount; index++)
+ {
+ if (profile == AzureHistoryProfile.SparseEligibility && index % 10 != 0)
+ {
+ messages.Add(CreateIneligibleMessage(index));
+
+ continue;
+ }
+
+ messages.Add(CreateEligibleMessage(
+ index,
+ eligibleIndex,
+ includeImages,
+ imageBytes));
+ eligibleIndex++;
+ }
+
+ return messages;
+ }
+
+ ///
+ /// Verifies exact SDK message types, content ordering, text, media types, and image bytes.
+ ///
+ /// The captured production result.
+ /// The bounded-tail candidate result.
+ public static void EnsureEquivalent(
+ List legacy,
+ List candidate)
+ {
+ if (legacy.Count != candidate.Count)
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different counts: {legacy.Count} and {candidate.Count}.");
+ }
+
+ for (var messageIndex = 0; messageIndex < legacy.Count; messageIndex++)
+ {
+ var legacyMessage = legacy[messageIndex];
+ var candidateMessage = candidate[messageIndex];
+
+ if (legacyMessage.GetType() != candidateMessage.GetType() ||
+ legacyMessage.Content.Count != candidateMessage.Content.Count)
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different messages at index {messageIndex}.");
+ }
+
+ for (var contentIndex = 0; contentIndex < legacyMessage.Content.Count; contentIndex++)
+ {
+ var legacyContent = legacyMessage.Content[contentIndex];
+ var candidateContent = candidateMessage.Content[contentIndex];
+
+ if (legacyContent.Kind != candidateContent.Kind ||
+ legacyContent.Text != candidateContent.Text ||
+ legacyContent.ImageBytesMediaType != candidateContent.ImageBytesMediaType)
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different content at message {messageIndex}, part {contentIndex}.");
+ }
+
+ var legacyImage = legacyContent.ImageBytes;
+ var candidateImage = candidateContent.ImageBytes;
+
+ if ((legacyImage is null) != (candidateImage is null) ||
+ (legacyImage is not null &&
+ !legacyImage.ToMemory().Span.SequenceEqual(candidateImage.ToMemory().Span)))
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different image bytes at message {messageIndex}, part {contentIndex}.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Preserves the original production user-message conversion as the benchmark baseline.
+ ///
+ /// The raw user message.
+ /// The converted SDK user message, or .
+ private static UserChatMessage CreateUserMessageLegacy(AIChatMessage message)
+ {
+ if (message.Contents is not { Count: > 0 })
+ {
+ return string.IsNullOrWhiteSpace(message.Text)
+ ? null
+ : new UserChatMessage(message.Text);
+ }
+
+ var parts = new List();
+
+ foreach (var content in message.Contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent when !string.IsNullOrWhiteSpace(textContent.Text):
+ parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text));
+ break;
+ case DataContent dataContent when dataContent.Data is { Length: > 0 } && !string.IsNullOrWhiteSpace(dataContent.MediaType):
+ parts.Add(ChatMessageContentPart.CreateImagePart(
+ BinaryData.FromBytes(dataContent.Data.ToArray()),
+ dataContent.MediaType));
+ break;
+ }
+ }
+
+ if (parts.Count == 0)
+ {
+ return string.IsNullOrWhiteSpace(message.Text)
+ ? null
+ : new UserChatMessage(message.Text);
+ }
+
+ return new UserChatMessage(parts);
+ }
+
+ ///
+ /// Creates an eligible text or image message.
+ ///
+ /// The raw source index.
+ /// The eligible-message index.
+ /// Whether image messages are enabled.
+ /// The shared image source bytes.
+ /// The eligible message.
+ private static AIChatMessage CreateEligibleMessage(
+ int rawIndex,
+ int eligibleIndex,
+ bool includeImages,
+ byte[] imageBytes)
+ {
+ if (includeImages && eligibleIndex % 10 == 0)
+ {
+ return new AIChatMessage(
+ AIChatRole.User,
+ [
+ new TextContent($"image-{rawIndex}"),
+ new DataContent(imageBytes, "image/png"),
+ ]);
+ }
+
+ return new AIChatMessage(
+ eligibleIndex % 2 == 0
+ ? AIChatRole.User
+ : AIChatRole.Assistant,
+ $"message-{rawIndex}");
+ }
+
+ ///
+ /// Creates one of the ineligible role or content shapes used by the sparse profile.
+ ///
+ /// The raw source index.
+ /// The ineligible message.
+ private static AIChatMessage CreateIneligibleMessage(int index)
+ {
+ return (index % 10) switch
+ {
+ 1 => new AIChatMessage(AIChatRole.System, $"system-{index}"),
+ 2 => new AIChatMessage(AIChatRole.Tool, $"tool-{index}"),
+ 3 => new AIChatMessage(new AIChatRole("observer"), $"unknown-{index}"),
+ 4 => new AIChatMessage(AIChatRole.User, (string)null),
+ 5 => new AIChatMessage(AIChatRole.Assistant, " \t"),
+ 6 => new AIChatMessage(AIChatRole.User, [new UnsupportedBenchmarkContent()]),
+ 7 => new AIChatMessage(
+ AIChatRole.Assistant,
+ [new DataContent(new byte[] { 1 }, "image/png")]),
+ 8 => new AIChatMessage(
+ AIChatRole.User,
+ [new DataContent(ReadOnlyMemory.Empty, "image/png")]),
+ _ => new AIChatMessage(AIChatRole.User, string.Empty),
+ };
+ }
+
+ ///
+ /// Creates a strongly typed delegate for the production prompt-selection helper.
+ ///
+ /// The prompt-selection delegate.
+ private static Func, List> CreateGetPromptsDelegate()
+ {
+ var method = typeof(AzureOpenAICompletionClient).GetMethod(
+ "GetPrompts",
+ BindingFlags.NonPublic | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Unable to find the prompt-selection helper.");
+
+ return method.CreateDelegate, List>>();
+ }
+
+ ///
+ /// Represents unsupported content used by the sparse benchmark distribution.
+ ///
+ private sealed class UnsupportedBenchmarkContent : AIContent
+ {
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAIStreamingUpdateConversionBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAIStreamingUpdateConversionBenchmarks.cs
new file mode 100644
index 00000000..113f9e2c
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/AzureOpenAIStreamingUpdateConversionBenchmarks.cs
@@ -0,0 +1,191 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.OpenAI.Azure.Services;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured Azure streaming update conversion with the production converter. This
+/// class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class AzureOpenAIStreamingUpdateConversionBenchmarks
+{
+ private StreamingChatCompletionUpdate[] _updates;
+
+ ///
+ /// Gets or sets the number of text content parts in each streaming update.
+ ///
+ [Params(1, 8, 64)]
+ public int ContentPartCount { get; set; }
+
+ ///
+ /// Gets or sets the number of streaming updates converted per operation.
+ ///
+ [Params(1_000)]
+ public int UpdateCount { get; set; }
+
+ ///
+ /// Creates stable streaming updates and verifies legacy/current equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _updates = CreateUpdates(UpdateCount, ContentPartCount);
+
+ var legacy = Legacy();
+ var current = Current();
+
+ EnsureEquivalent(legacy, current);
+ }
+
+ ///
+ /// Converts streaming update content with the legacy LINQ projection chain.
+ ///
+ /// The converted updates.
+ [Benchmark(Baseline = true)]
+ public ChatResponseUpdate[] Legacy()
+ {
+ var results = new ChatResponseUpdate[_updates.Length];
+
+ for (var index = 0; index < _updates.Length; index++)
+ {
+ results[index] = CreateLegacyStreamingResponseUpdate(_updates[index]);
+ }
+
+ return results;
+ }
+
+ ///
+ /// Converts streaming update content with the production converter.
+ ///
+ /// The converted updates.
+ [Benchmark]
+ public ChatResponseUpdate[] Current()
+ {
+ var results = new ChatResponseUpdate[_updates.Length];
+
+ for (var index = 0; index < _updates.Length; index++)
+ {
+ results[index] = AzureOpenAICompletionClient.CreateStreamingResponseUpdate(_updates[index]);
+ }
+
+ return results;
+ }
+
+ ///
+ /// Reproduces the previous production streaming update conversion.
+ ///
+ /// The Azure streaming update.
+ /// The converted framework update.
+ private static ChatResponseUpdate CreateLegacyStreamingResponseUpdate(StreamingChatCompletionUpdate update)
+ {
+ var result = new ChatResponseUpdate
+ {
+ ResponseId = update.CompletionId,
+ CreatedAt = update.CreatedAt,
+ ModelId = update.Model,
+ Contents = update.ContentUpdate.Select(x => new TextContent(x.Text)).Cast().ToList(),
+ };
+
+ if (update.FinishReason is not null)
+ {
+ result.FinishReason = new Microsoft.Extensions.AI.ChatFinishReason(update.FinishReason?.ToString());
+ }
+
+ if (update.Role is not null)
+ {
+ result.Role = new Microsoft.Extensions.AI.ChatRole(update.Role.ToString().ToLowerInvariant());
+ }
+
+ return result;
+ }
+
+ ///
+ /// Creates benchmark streaming updates.
+ ///
+ /// The number of updates.
+ /// The number of content parts in each update.
+ /// The benchmark updates.
+ private static StreamingChatCompletionUpdate[] CreateUpdates(
+ int updateCount,
+ int contentPartCount)
+ {
+ var updates = new StreamingChatCompletionUpdate[updateCount];
+ var createdAt = new DateTimeOffset(2026, 7, 16, 1, 2, 3, TimeSpan.Zero);
+
+ for (var updateIndex = 0; updateIndex < updateCount; updateIndex++)
+ {
+ var parts = new ChatMessageContentPart[contentPartCount];
+
+ for (var partIndex = 0; partIndex < parts.Length; partIndex++)
+ {
+ parts[partIndex] = ChatMessageContentPart.CreateTextPart($"update-{updateIndex}-part-{partIndex}");
+ }
+
+ updates[updateIndex] = OpenAIChatModelFactory.StreamingChatCompletionUpdate(
+ completionId: $"completion-{updateIndex}",
+ contentUpdate: new ChatMessageContent(parts),
+ functionCallUpdate: null,
+ toolCallUpdates: [],
+ role: updateIndex % 2 == 0 ? ChatMessageRole.Assistant : null,
+ refusalUpdate: null,
+ contentTokenLogProbabilities: [],
+ refusalTokenLogProbabilities: [],
+ finishReason: updateIndex % 10 == 0 ? OpenAI.Chat.ChatFinishReason.Stop : null,
+ createdAt: createdAt.AddMilliseconds(updateIndex),
+ model: "model-1",
+ systemFingerprint: "fingerprint-1",
+ usage: null);
+ }
+
+ return updates;
+ }
+
+ ///
+ /// Verifies exact converted update equivalence.
+ ///
+ /// The legacy converted updates.
+ /// The production converted updates.
+ private static void EnsureEquivalent(
+ ChatResponseUpdate[] legacy,
+ ChatResponseUpdate[] current)
+ {
+ if (legacy.Length != current.Length)
+ {
+ throw new InvalidOperationException("The streaming conversion implementations returned different counts.");
+ }
+
+ for (var updateIndex = 0; updateIndex < legacy.Length; updateIndex++)
+ {
+ var legacyUpdate = legacy[updateIndex];
+ var currentUpdate = current[updateIndex];
+
+ if (legacyUpdate.ResponseId != currentUpdate.ResponseId ||
+ legacyUpdate.CreatedAt != currentUpdate.CreatedAt ||
+ legacyUpdate.ModelId != currentUpdate.ModelId ||
+ legacyUpdate.FinishReason?.ToString() != currentUpdate.FinishReason?.ToString() ||
+ legacyUpdate.Role?.ToString() != currentUpdate.Role?.ToString() ||
+ legacyUpdate.Contents.Count != currentUpdate.Contents.Count)
+ {
+ throw new InvalidOperationException($"Streaming update metadata differed at index {updateIndex}.");
+ }
+
+ for (var contentIndex = 0; contentIndex < legacyUpdate.Contents.Count; contentIndex++)
+ {
+ var legacyText = ((TextContent)legacyUpdate.Contents[contentIndex]).Text;
+ var currentText = ((TextContent)currentUpdate.Contents[contentIndex]).Text;
+
+ if (legacyText != currentText)
+ {
+ throw new InvalidOperationException($"Streaming update content differed at update {updateIndex}, content {contentIndex}.");
+ }
+ }
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/ChatConversationHistoryBuilderBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/ChatConversationHistoryBuilderBenchmarks.cs
new file mode 100644
index 00000000..92867d98
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/ChatConversationHistoryBuilderBenchmarks.cs
@@ -0,0 +1,343 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Chat.Hubs;
+using CrestApps.Core.AI.Models;
+using Microsoft.Extensions.AI;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured hub conversation-history construction with the shared production helper.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class ChatConversationHistoryBuilderBenchmarks
+{
+ private static readonly DateTime _originUtc =
+ new(2026, 7, 13, 12, 0, 0, DateTimeKind.Utc);
+
+ private AIChatSessionPrompt[] _chatSessionPrompts;
+ private AIChatSessionPrompt _chatSessionNewPrompt;
+ private ChatInteractionPrompt[] _interactionPrompts;
+ private ChatInteractionPrompt _interactionNewPrompt;
+
+ ///
+ /// Gets or sets the prompt model used by the hub path.
+ ///
+ [Params(PromptHistoryModel.ChatInteraction, PromptHistoryModel.AIChatSession)]
+ public PromptHistoryModel Model { get; set; }
+
+ ///
+ /// Gets or sets the number of stored prompts.
+ ///
+ [Params(10, 100, 1_000, 10_000)]
+ public int PromptCount { get; set; }
+
+ ///
+ /// Gets or sets the generated-prompt distribution.
+ ///
+ [Params(GeneratedPromptDistribution.None, GeneratedPromptDistribution.Quarter)]
+ public GeneratedPromptDistribution GeneratedPrompts { get; set; }
+
+ ///
+ /// Gets or sets whether the stored result already contains the newly created prompt identifier.
+ ///
+ [Params(false, true)]
+ public bool NewPromptPresent { get; set; }
+
+ ///
+ /// Gets or sets whether the store result follows or violates its creation-time ordering contract.
+ ///
+ [Params(PromptHistoryOrdering.Ordered, PromptHistoryOrdering.Unordered)]
+ public PromptHistoryOrdering Ordering { get; set; }
+
+ ///
+ /// Creates stable benchmark inputs and verifies exact output equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var specifications = CreatePromptSpecifications();
+ var newPromptId = NewPromptPresent
+ ? specifications[^1].ItemId
+ : "new-prompt";
+ var newPromptSpecification = new PromptSpecification(
+ newPromptId,
+ ChatRole.User,
+ "new prompt",
+ _originUtc.AddMinutes(PromptCount + 1),
+ IsGenerated: false);
+
+ _interactionPrompts = specifications
+ .Select(CreateInteractionPrompt)
+ .ToArray();
+ _interactionNewPrompt = CreateInteractionPrompt(newPromptSpecification);
+ _chatSessionPrompts = specifications
+ .Select(CreateChatSessionPrompt)
+ .ToArray();
+ _chatSessionNewPrompt = CreateChatSessionPrompt(newPromptSpecification);
+
+ EnsureEquivalent(Legacy(), Current());
+ }
+
+ ///
+ /// Builds history using the captured per-hub materialize, search, stable sort, filter, and
+ /// projection pipeline.
+ ///
+ /// The projected conversation history.
+ [Benchmark(Baseline = true)]
+ public List Legacy()
+ {
+ return Model switch
+ {
+ PromptHistoryModel.ChatInteraction => BuildLegacy(
+ _interactionPrompts,
+ _interactionNewPrompt),
+ PromptHistoryModel.AIChatSession => BuildLegacy(
+ _chatSessionPrompts,
+ _chatSessionNewPrompt),
+ _ => throw new InvalidOperationException(
+ $"Unsupported prompt history model '{Model}'."),
+ };
+ }
+
+ ///
+ /// Builds history using the shared production helper.
+ ///
+ /// The projected conversation history.
+ [Benchmark]
+ public List Current()
+ {
+ return Model switch
+ {
+ PromptHistoryModel.ChatInteraction =>
+ ChatConversationHistoryBuilder.Build(
+ _interactionPrompts,
+ _interactionNewPrompt),
+ PromptHistoryModel.AIChatSession =>
+ ChatConversationHistoryBuilder.Build(
+ _chatSessionPrompts,
+ _chatSessionNewPrompt),
+ _ => throw new InvalidOperationException(
+ $"Unsupported prompt history model '{Model}'."),
+ };
+ }
+
+ ///
+ /// Preserves the original chat interaction hub implementation as the benchmark baseline.
+ ///
+ /// The stored prompts.
+ /// The newly created prompt.
+ /// The projected conversation history.
+ private static List BuildLegacy(
+ IEnumerable prompts,
+ ChatInteractionPrompt newPrompt)
+ {
+ var conversationHistorySource = prompts.ToList();
+
+ if (!conversationHistorySource.Any(prompt =>
+ prompt.ItemId == newPrompt.ItemId))
+ {
+ conversationHistorySource.Add(newPrompt);
+ }
+
+ return conversationHistorySource
+ .OrderBy(prompt => prompt.CreatedUtc)
+ .Where(prompt => !prompt.IsGeneratedPrompt)
+ .Select(prompt => new ChatMessage(prompt.Role, prompt.Text))
+ .ToList();
+ }
+
+ ///
+ /// Preserves the original AI chat session hub implementation as the benchmark baseline.
+ ///
+ /// The stored prompts.
+ /// The newly created prompt.
+ /// The projected conversation history.
+ private static List BuildLegacy(
+ IEnumerable prompts,
+ AIChatSessionPrompt newPrompt)
+ {
+ var conversationHistorySource = prompts.ToList();
+
+ if (!conversationHistorySource.Any(prompt =>
+ prompt.ItemId == newPrompt.ItemId))
+ {
+ conversationHistorySource.Add(newPrompt);
+ }
+
+ return conversationHistorySource
+ .OrderBy(prompt => prompt.CreatedUtc)
+ .Where(prompt => !prompt.IsGeneratedPrompt)
+ .Select(prompt => new ChatMessage(prompt.Role, prompt.Content))
+ .ToList();
+ }
+
+ ///
+ /// Creates ordered or deliberately unordered prompt specifications with equal-timestamp pairs.
+ ///
+ /// The benchmark prompt specifications.
+ private PromptSpecification[] CreatePromptSpecifications()
+ {
+ var generatedCount = GeneratedPrompts == GeneratedPromptDistribution.Quarter
+ ? (int)Math.Round(PromptCount * 0.25, MidpointRounding.AwayFromZero)
+ : 0;
+ var specifications = new PromptSpecification[PromptCount];
+
+ for (var index = 0; index < specifications.Length; index++)
+ {
+ specifications[index] = new PromptSpecification(
+ $"prompt-{index:D5}",
+ index % 2 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant,
+ $"prompt text {index}",
+ _originUtc.AddMinutes(index / 2),
+ IsGenerated: index < generatedCount);
+ }
+
+ if (Ordering == PromptHistoryOrdering.Ordered)
+ {
+ return specifications;
+ }
+
+ var unordered = new PromptSpecification[specifications.Length];
+
+ for (var index = 0; index < unordered.Length; index++)
+ {
+ unordered[index] = specifications[(index * 37) % specifications.Length];
+ }
+
+ return unordered;
+ }
+
+ ///
+ /// Creates a chat interaction prompt.
+ ///
+ /// The prompt specification.
+ /// The prompt.
+ private static ChatInteractionPrompt CreateInteractionPrompt(
+ PromptSpecification prompt)
+ {
+ return new()
+ {
+ ItemId = prompt.ItemId,
+ ChatInteractionId = "interaction",
+ Role = prompt.Role,
+ Text = prompt.Text,
+ CreatedUtc = prompt.CreatedUtc,
+ IsGeneratedPrompt = prompt.IsGenerated,
+ };
+ }
+
+ ///
+ /// Creates an AI chat session prompt.
+ ///
+ /// The prompt specification.
+ /// The prompt.
+ private static AIChatSessionPrompt CreateChatSessionPrompt(
+ PromptSpecification prompt)
+ {
+ return new()
+ {
+ ItemId = prompt.ItemId,
+ SessionId = "session",
+ Role = prompt.Role,
+ Content = prompt.Text,
+ CreatedUtc = prompt.CreatedUtc,
+ IsGeneratedPrompt = prompt.IsGenerated,
+ };
+ }
+
+ ///
+ /// Verifies exact message count, stable order, role, and text equivalence.
+ ///
+ /// The captured legacy output.
+ /// The production output.
+ private static void EnsureEquivalent(
+ List legacy,
+ List current)
+ {
+ if (legacy.Count != current.Count)
+ {
+ throw new InvalidOperationException(
+ $"History implementations returned different counts: {legacy.Count} and {current.Count}.");
+ }
+
+ for (var index = 0; index < legacy.Count; index++)
+ {
+ if (legacy[index].Role != current[index].Role ||
+ legacy[index].Text != current[index].Text)
+ {
+ throw new InvalidOperationException(
+ $"History implementations returned different messages at index {index}.");
+ }
+ }
+ }
+
+ ///
+ /// Describes one benchmark prompt independently of its stored model.
+ ///
+ /// The prompt identifier.
+ /// The prompt role.
+ /// The prompt text.
+ /// The creation timestamp.
+ /// Whether the prompt is generated.
+ private sealed record PromptSpecification(
+ string ItemId,
+ ChatRole Role,
+ string Text,
+ DateTime CreatedUtc,
+ bool IsGenerated);
+
+ ///
+ /// Defines the hub prompt model under test.
+ ///
+ public enum PromptHistoryModel
+ {
+ ///
+ /// Uses .
+ ///
+ ChatInteraction,
+
+ ///
+ /// Uses .
+ ///
+ AIChatSession,
+ }
+
+ ///
+ /// Defines the generated-prompt distribution.
+ ///
+ public enum GeneratedPromptDistribution
+ {
+ ///
+ /// No stored prompts are generated.
+ ///
+ None,
+
+ ///
+ /// Approximately one quarter of stored prompts are generated.
+ ///
+ Quarter,
+ }
+
+ ///
+ /// Defines whether the store result follows its public ordering contract.
+ ///
+ public enum PromptHistoryOrdering
+ {
+ ///
+ /// Prompts are ordered by creation time.
+ ///
+ Ordered,
+
+ ///
+ /// Prompts deliberately violate creation-time ordering.
+ ///
+ Unordered,
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/CitationReferenceRescanBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/CitationReferenceRescanBenchmarks.cs
new file mode 100644
index 00000000..c8376600
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/CitationReferenceRescanBenchmarks.cs
@@ -0,0 +1,215 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Chat.Services;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.Orchestration;
+using CrestApps.Core.AI.Profiles;
+using CrestApps.Core.AI.Services;
+using CrestApps.Core.Infrastructure.Indexing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Quantifies the cost of the full-dictionary rescan against
+/// a hypothetical incremental rescan that resolves only newly added references. Streaming responses
+/// invoke the collector once per tool-reference add-event, so the production path re-scans the whole
+/// reference map on each event. This benchmark isolates where a material gain would exist (references
+/// whose link never resolves) from where the current path is already optimal (references that resolve
+/// on their first pass). This class must remain unsealed because BenchmarkDotNet generates a derived
+/// benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class CitationReferenceRescanBenchmarks
+{
+ private const string BenchReferenceType = "benchmark-reference";
+
+ private CompositeAIReferenceLinkResolver _composite;
+ private CitationReferenceCollector _collector;
+ private (string Key, AICompletionReference Reference)[] _events;
+
+ ///
+ /// Gets or sets whether the resolver returns a link (resolved) or null (unresolved).
+ ///
+ [Params(ReferenceResolution.Resolved, ReferenceResolution.Unresolved)]
+ public ReferenceResolution Resolution { get; set; }
+
+ ///
+ /// Gets or sets the number of references that accumulate across streaming add-events.
+ ///
+ [Params(16, 64, 256)]
+ public int ReferenceCount { get; set; }
+
+ ///
+ /// Builds stable benchmark inputs and verifies the incremental candidate produces output that is
+ /// identical to the production collector for a deterministic resolver.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var returnsLink = Resolution == ReferenceResolution.Resolved;
+ var services = new ServiceCollection();
+ services.AddKeyedSingleton(
+ BenchReferenceType,
+ new BenchmarkReferenceLinkResolver(returnsLink));
+ services.AddSingleton();
+ var serviceProvider = services.BuildServiceProvider();
+
+ _composite = serviceProvider.GetRequiredService();
+ _collector = new CitationReferenceCollector(_composite);
+ _events = new (string, AICompletionReference)[ReferenceCount];
+
+ for (var index = 0; index < ReferenceCount; index++)
+ {
+ var key = $"[ref-{index}]";
+ _events[index] = (key, new AICompletionReference
+ {
+ Index = index + 1,
+ ReferenceId = $"ref-{index}",
+ ReferenceType = BenchReferenceType,
+ Title = $"Reference {index}",
+ });
+ }
+
+ EnsureEquivalent(Current(), Candidate());
+ }
+
+ ///
+ /// Reproduces the production streaming path: add one tool reference per event and re-run the
+ /// collector, which rescans the entire reference map on every event.
+ ///
+ /// The accumulated references keyed by citation token.
+ [Benchmark(Baseline = true)]
+ public Dictionary Current()
+ {
+ var references = new Dictionary();
+ var contentItemIds = new HashSet();
+
+ using var scope = AIInvocationScope.Begin();
+ var toolReferences = scope.Context.ToolReferences;
+
+ for (var index = 0; index < _events.Length; index++)
+ {
+ var (key, template) = _events[index];
+ toolReferences[key] = Clone(template);
+ _collector.CollectToolReferences(references, contentItemIds);
+ }
+
+ return references;
+ }
+
+ ///
+ /// Models an incremental rescan that resolves only the newly added reference. This changes the
+ /// resolver call-count for unresolved references from quadratic to linear.
+ ///
+ /// The accumulated references keyed by citation token.
+ [Benchmark]
+ public Dictionary Candidate()
+ {
+ var references = new Dictionary();
+ var contentItemIds = new HashSet();
+
+ for (var index = 0; index < _events.Length; index++)
+ {
+ var (key, template) = _events[index];
+
+ if (references.TryAdd(key, Clone(template)))
+ {
+ ResolveSingle(references[key], contentItemIds);
+ }
+ }
+
+ return references;
+ }
+
+ private static AICompletionReference Clone(AICompletionReference template)
+ {
+ return new AICompletionReference
+ {
+ Index = template.Index,
+ ReferenceId = template.ReferenceId,
+ ReferenceType = template.ReferenceType,
+ Title = template.Title,
+ };
+ }
+
+ private void ResolveSingle(AICompletionReference reference, HashSet contentItemIds)
+ {
+ if (string.IsNullOrEmpty(reference.Link) &&
+ !string.IsNullOrEmpty(reference.ReferenceId) &&
+ !string.IsNullOrEmpty(reference.ReferenceType))
+ {
+ reference.Link = _composite.ResolveLink(
+ reference.ReferenceId,
+ reference.ReferenceType,
+ new Dictionary
+ {
+ ["Title"] = reference.Title,
+ });
+ }
+
+ if (!string.IsNullOrEmpty(reference.ReferenceId) &&
+ string.Equals(reference.ReferenceType, IndexProfileTypes.Articles, StringComparison.OrdinalIgnoreCase))
+ {
+ contentItemIds.Add(reference.ReferenceId);
+ }
+ }
+
+ private static void EnsureEquivalent(
+ Dictionary current,
+ Dictionary candidate)
+ {
+ if (current.Count != candidate.Count)
+ {
+ throw new InvalidOperationException(
+ $"Reference count mismatch: current={current.Count}, candidate={candidate.Count}.");
+ }
+
+ foreach (var (key, reference) in current)
+ {
+ if (!candidate.TryGetValue(key, out var other) ||
+ !string.Equals(reference.Link, other.Link, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Resolved link mismatch for reference '{key}'.");
+ }
+ }
+ }
+
+ ///
+ /// Identifies whether the benchmark resolver produces a link or leaves the reference unresolved.
+ ///
+ public enum ReferenceResolution
+ {
+ ///
+ /// The resolver returns a non-empty link, so each reference is resolved exactly once.
+ ///
+ Resolved,
+
+ ///
+ /// The resolver returns null, so the production path re-attempts the reference on
+ /// every subsequent rescan.
+ ///
+ Unresolved,
+ }
+
+ private sealed class BenchmarkReferenceLinkResolver : IAIReferenceLinkResolver
+ {
+ private readonly bool _returnsLink;
+
+ public BenchmarkReferenceLinkResolver(bool returnsLink)
+ {
+ _returnsLink = returnsLink;
+ }
+
+ public string ResolveLink(string referenceId, IDictionary metadata)
+ {
+ return _returnsLink
+ ? $"/link/{referenceId}"
+ : null;
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/ClaudeOrchestratorPromptsBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/ClaudeOrchestratorPromptsBenchmarks.cs
new file mode 100644
index 00000000..de93b3ee
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/ClaudeOrchestratorPromptsBenchmarks.cs
@@ -0,0 +1,262 @@
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Claude.Services;
+using CrestApps.Core.AI.Models;
+using Microsoft.Extensions.AI;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured materialize-all-before-tail prompt construction with the production
+/// bounded-ring implementation of across bounded and unbounded
+/// history settings for dense and sparse eligibility. This class must remain unsealed because
+/// BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[ShortRunJob]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class ClaudeOrchestratorPromptsBenchmarks
+{
+ private const string SystemMessage = "benchmark-system";
+ private const string UserMessage = "benchmark-user";
+
+ private static readonly Func> _legacy = BuildPromptsLegacy;
+ private static readonly Func> _current = CreateBuildPromptsDelegate();
+
+ private OrchestrationContext _context;
+ private List _messages;
+
+ ///
+ /// Gets or sets the total number of source messages.
+ ///
+ [Params(10, 1_000, 10_000)]
+ public int MessageCount { get; set; }
+
+ ///
+ /// Gets or sets the configured number of past messages.
+ ///
+ [Params(1, 2, 20, 200)]
+ public int PastMessagesCount { get; set; }
+
+ ///
+ /// Gets or sets the eligible-message density.
+ ///
+ [Params(PromptEligibility.Dense, PromptEligibility.Sparse)]
+ public PromptEligibility Eligibility { get; set; }
+
+ ///
+ /// Creates immutable benchmark inputs and verifies exact output equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _messages = CreateMessages(MessageCount, Eligibility);
+ _context = CreateContext(_messages, PastMessagesCount);
+
+ EnsureEquivalent(_legacy(_context), _current(_context));
+ }
+
+ ///
+ /// Constructs prompts using the captured materialize-all-before-tail implementation.
+ ///
+ /// The constructed prompts.
+ [Benchmark(Baseline = true)]
+ public List Legacy()
+ {
+ return _legacy(_context);
+ }
+
+ ///
+ /// Constructs prompts using the production bounded-ring implementation.
+ ///
+ /// The constructed prompts.
+ [Benchmark]
+ public List Current()
+ {
+ return _current(_context);
+ }
+
+ ///
+ /// Preserves the original materialize-all-before-tail prompt construction as the benchmark baseline.
+ ///
+ /// The orchestration context.
+ /// The constructed prompts.
+ private static List BuildPromptsLegacy(OrchestrationContext context)
+ {
+ var prompts = new List();
+ var systemMessage = context.CompletionContext.SystemMessage;
+
+ if (!string.IsNullOrWhiteSpace(systemMessage))
+ {
+ prompts.Add(new ChatMessage(ChatRole.System, systemMessage));
+ }
+
+ var history = context.ConversationHistory?
+ .Where(message => message.Role == ChatRole.User || message.Role == ChatRole.Assistant)
+ .Where(message => !string.IsNullOrWhiteSpace(message.Text))
+ .ToList() ?? [];
+
+ if (context.CompletionContext.PastMessagesCount > 1)
+ {
+ var count = Math.Min(history.Count, context.CompletionContext.PastMessagesCount.Value);
+ prompts.AddRange(history.Skip(Math.Max(0, history.Count - count)));
+ }
+ else
+ {
+ prompts.AddRange(history);
+ }
+
+ if (prompts.Count == 0 || prompts[^1].Text != context.UserMessage)
+ {
+ prompts.Add(new ChatMessage(ChatRole.User, context.UserMessage));
+ }
+
+ return prompts;
+ }
+
+ ///
+ /// Creates an orchestration context that carries the benchmark history and settings.
+ ///
+ /// The conversation history.
+ /// The configured number of past messages.
+ /// The orchestration context.
+ private static OrchestrationContext CreateContext(List messages, int pastMessagesCount)
+ {
+ return new OrchestrationContext
+ {
+ UserMessage = UserMessage,
+ ConversationHistory = messages,
+ CompletionContext = new AICompletionContext
+ {
+ SystemMessage = SystemMessage,
+ PastMessagesCount = pastMessagesCount,
+ },
+ };
+ }
+
+ ///
+ /// Creates source messages with the requested eligibility density.
+ ///
+ /// The total message count.
+ /// The eligible-message density.
+ /// The source messages.
+ private static List CreateMessages(
+ int messageCount,
+ PromptEligibility eligibility)
+ {
+ var messages = new List(messageCount);
+
+ for (var index = 0; index < messageCount; index++)
+ {
+ if (eligibility == PromptEligibility.Dense)
+ {
+ var role = index % 2 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant;
+
+ messages.Add(new ChatMessage(role, $"message-{index}"));
+
+ continue;
+ }
+
+ messages.Add(CreateSparseMessage(index));
+ }
+
+ return messages;
+ }
+
+ ///
+ /// Creates a sparse-distribution message with one eligible message per ten source messages.
+ ///
+ /// The source index.
+ /// The sparse-distribution message.
+ private static ChatMessage CreateSparseMessage(int index)
+ {
+ return (index % 10) switch
+ {
+ 0 => new ChatMessage(
+ index % 20 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant,
+ $"eligible-{index}"),
+ 1 or 6 => new ChatMessage(ChatRole.System, $"system-{index}"),
+ 2 or 7 => new ChatMessage(ChatRole.Tool, $"tool-{index}"),
+ 3 or 8 => new ChatMessage(new ChatRole("observer"), $"unknown-{index}"),
+ _ => new ChatMessage(
+ index % 2 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant,
+ (string)null),
+ };
+ }
+
+ ///
+ /// Verifies exact values, ordering, and history source-message identity.
+ ///
+ /// The legacy result.
+ /// The production result.
+ private static void EnsureEquivalent(
+ List legacy,
+ List current)
+ {
+ if (legacy.Count != current.Count)
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different counts: {legacy.Count} and {current.Count}.");
+ }
+
+ for (var index = 0; index < legacy.Count; index++)
+ {
+ var legacyMessage = legacy[index];
+ var currentMessage = current[index];
+
+ if (legacyMessage.Role != currentMessage.Role ||
+ legacyMessage.Text != currentMessage.Text ||
+ legacyMessage.Contents.Count != currentMessage.Contents.Count)
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different values at index {index}.");
+ }
+
+ if (index > 0 &&
+ index < legacy.Count - 1 &&
+ !ReferenceEquals(legacyMessage, currentMessage))
+ {
+ throw new InvalidOperationException(
+ $"Prompt implementations returned different source-message references at index {index}.");
+ }
+ }
+ }
+
+ ///
+ /// Creates a strongly typed delegate for the private production prompt-construction helper.
+ ///
+ /// The production prompt-construction delegate.
+ private static Func> CreateBuildPromptsDelegate()
+ {
+ var method = typeof(ClaudeOrchestrator).GetMethod(
+ "BuildPrompts",
+ BindingFlags.NonPublic | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Unable to find the prompt-construction helper.");
+
+ return method.CreateDelegate>>();
+ }
+
+ ///
+ /// Defines the eligible-message density.
+ ///
+ public enum PromptEligibility
+ {
+ ///
+ /// Every source message is eligible.
+ ///
+ Dense,
+
+ ///
+ /// One in ten source messages is eligible.
+ ///
+ Sparse,
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/ContextBuilderReverseDispatchBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/ContextBuilderReverseDispatchBenchmarks.cs
new file mode 100644
index 00000000..d72afd25
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/ContextBuilderReverseDispatchBenchmarks.cs
@@ -0,0 +1,402 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Completions;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.Orchestration;
+using CrestApps.Core.AI.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured reverse-enumeration context builders with production's exact
+/// per-phase array snapshots. This class must remain unsealed because BenchmarkDotNet
+/// generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[CategoriesColumn]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+public class ContextBuilderReverseDispatchBenchmarks
+{
+ private static readonly object _resource = new();
+ private LegacyOrchestrationContextBuilder _legacyOrchestrationBuilder;
+ private DefaultOrchestrationContextBuilder _currentOrchestrationBuilder;
+ private LegacyAICompletionContextBuilder _legacyCompletionBuilder;
+ private DefaultAICompletionContextBuilder _currentCompletionBuilder;
+
+ ///
+ /// Gets or sets the number of handlers dispatched in each build phase.
+ ///
+ [Params(0, 1, 4, 16, 64)]
+ public int HandlerCount { get; set; }
+
+ ///
+ /// Creates independent legacy and production builders and verifies exact phase ordering.
+ ///
+ /// A task that completes after setup.
+ [GlobalSetup]
+ public async Task Setup()
+ {
+ await VerifyOrchestrationEquivalenceAsync();
+ await VerifyCompletionEquivalenceAsync();
+
+ var legacyOrchestrationHandlers = CreateOrchestrationHandlers();
+ var currentOrchestrationHandlers = CreateOrchestrationHandlers();
+ var serviceProvider = new EmptyServiceProvider();
+
+ _legacyOrchestrationBuilder = new LegacyOrchestrationContextBuilder(
+ legacyOrchestrationHandlers,
+ serviceProvider,
+ NullLogger.Instance);
+ _currentOrchestrationBuilder = new DefaultOrchestrationContextBuilder(
+ currentOrchestrationHandlers,
+ serviceProvider,
+ NullLogger.Instance);
+
+ _legacyCompletionBuilder = new LegacyAICompletionContextBuilder(
+ CreateCompletionHandlers(),
+ NullLogger.Instance);
+ _currentCompletionBuilder = new DefaultAICompletionContextBuilder(
+ CreateCompletionHandlers(),
+ NullLogger.Instance);
+ }
+
+ ///
+ /// Builds an orchestration context with the captured implementation.
+ ///
+ /// The built orchestration context.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Orchestration")]
+ public ValueTask LegacyOrchestration()
+ {
+ return _legacyOrchestrationBuilder.BuildAsync(_resource);
+ }
+
+ ///
+ /// Builds an orchestration context with production.
+ ///
+ /// The built orchestration context.
+ [Benchmark]
+ [BenchmarkCategory("Orchestration")]
+ public ValueTask CurrentOrchestration()
+ {
+ return _currentOrchestrationBuilder.BuildAsync(_resource);
+ }
+
+ ///
+ /// Builds a completion context with the captured implementation.
+ ///
+ /// The built completion context.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("Completion")]
+ public ValueTask LegacyCompletion()
+ {
+ return _legacyCompletionBuilder.BuildAsync(_resource);
+ }
+
+ ///
+ /// Builds a completion context with production.
+ ///
+ /// The built completion context.
+ [Benchmark]
+ [BenchmarkCategory("Completion")]
+ public ValueTask CurrentCompletion()
+ {
+ return _currentCompletionBuilder.BuildAsync(_resource);
+ }
+
+ private IOrchestrationContextBuilderHandler[] CreateOrchestrationHandlers()
+ {
+ return Enumerable.Range(0, HandlerCount)
+ .Select(_ => (IOrchestrationContextBuilderHandler)new NoOpOrchestrationHandler())
+ .ToArray();
+ }
+
+ private IAICompletionContextBuilderHandler[] CreateCompletionHandlers()
+ {
+ return Enumerable.Range(0, HandlerCount)
+ .Select(_ => (IAICompletionContextBuilderHandler)new NoOpCompletionHandler())
+ .ToArray();
+ }
+
+ private async Task VerifyOrchestrationEquivalenceAsync()
+ {
+ var legacyTrace = new List();
+ var currentTrace = new List();
+ var serviceProvider = new EmptyServiceProvider();
+ var legacyHandlers = Enumerable.Range(0, HandlerCount)
+ .Select(index => (IOrchestrationContextBuilderHandler)new RecordingOrchestrationHandler(index, legacyTrace))
+ .ToArray();
+ var currentHandlers = Enumerable.Range(0, HandlerCount)
+ .Select(index => (IOrchestrationContextBuilderHandler)new RecordingOrchestrationHandler(index, currentTrace))
+ .ToArray();
+ var legacy = new LegacyOrchestrationContextBuilder(
+ legacyHandlers,
+ serviceProvider,
+ NullLogger.Instance);
+ var current = new DefaultOrchestrationContextBuilder(
+ currentHandlers,
+ serviceProvider,
+ NullLogger.Instance);
+
+ var legacyContext = await legacy.BuildAsync(_resource);
+ var currentContext = await current.BuildAsync(_resource);
+
+ if (!legacyTrace.SequenceEqual(currentTrace) ||
+ legacyContext.ServiceProvider != currentContext.ServiceProvider ||
+ legacyContext.CompletionContext != currentContext.CompletionContext)
+ {
+ throw new InvalidOperationException("Orchestration builder behavior differs.");
+ }
+ }
+
+ private async Task VerifyCompletionEquivalenceAsync()
+ {
+ var legacyTrace = new List();
+ var currentTrace = new List();
+ var legacyHandlers = Enumerable.Range(0, HandlerCount)
+ .Select(index => (IAICompletionContextBuilderHandler)new RecordingCompletionHandler(index, legacyTrace))
+ .ToArray();
+ var currentHandlers = Enumerable.Range(0, HandlerCount)
+ .Select(index => (IAICompletionContextBuilderHandler)new RecordingCompletionHandler(index, currentTrace))
+ .ToArray();
+ var legacy = new LegacyAICompletionContextBuilder(
+ legacyHandlers,
+ NullLogger.Instance);
+ var current = new DefaultAICompletionContextBuilder(
+ currentHandlers,
+ NullLogger.Instance);
+
+ var legacyContext = await legacy.BuildAsync(_resource);
+ var currentContext = await current.BuildAsync(_resource);
+
+ if (!legacyTrace.SequenceEqual(currentTrace) ||
+ legacyContext.AdditionalProperties.Count != currentContext.AdditionalProperties.Count)
+ {
+ throw new InvalidOperationException("Completion builder behavior differs.");
+ }
+ }
+
+ private sealed class EmptyServiceProvider : IServiceProvider
+ {
+ public object GetService(Type serviceType)
+ {
+ return null;
+ }
+ }
+
+ private sealed class NoOpOrchestrationHandler : IOrchestrationContextBuilderHandler
+ {
+ public Task BuildingAsync(
+ OrchestrationContextBuildingContext context,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task BuiltAsync(
+ OrchestrationContextBuiltContext context,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class RecordingOrchestrationHandler(
+ int index,
+ List trace) : IOrchestrationContextBuilderHandler
+ {
+ public Task BuildingAsync(
+ OrchestrationContextBuildingContext context,
+ CancellationToken cancellationToken = default)
+ {
+ trace.Add(index);
+
+ return Task.CompletedTask;
+ }
+
+ public Task BuiltAsync(
+ OrchestrationContextBuiltContext context,
+ CancellationToken cancellationToken = default)
+ {
+ trace.Add(index + 10_000);
+
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class NoOpCompletionHandler : IAICompletionContextBuilderHandler
+ {
+ public Task BuildingAsync(AICompletionContextBuildingContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task BuiltAsync(AICompletionContextBuiltContext context)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class RecordingCompletionHandler(
+ int index,
+ List trace) : IAICompletionContextBuilderHandler
+ {
+ public Task BuildingAsync(AICompletionContextBuildingContext context)
+ {
+ trace.Add(index);
+
+ return Task.CompletedTask;
+ }
+
+ public Task BuiltAsync(AICompletionContextBuiltContext context)
+ {
+ trace.Add(index + 10_000);
+
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class LegacyOrchestrationContextBuilder
+ {
+ private readonly IEnumerable _handlers;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+
+ public LegacyOrchestrationContextBuilder(
+ IEnumerable handlers,
+ IServiceProvider serviceProvider,
+ ILogger logger)
+ {
+ _handlers = handlers?.Reverse() ?? [];
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ public async ValueTask BuildAsync(
+ object resource,
+ Action configure = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(resource);
+
+ var context = new OrchestrationContext
+ {
+ ServiceProvider = _serviceProvider,
+ };
+
+ var building = new OrchestrationContextBuildingContext(resource, context);
+
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ await handler.BuildingAsync(building, cancellationToken);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ _logger.LogError(ex, "Error in orchestration context building handler {Handler}.", handler.GetType().Name);
+ }
+ }
+
+ configure?.Invoke(context);
+
+ var built = new OrchestrationContextBuiltContext(resource, context);
+
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ await handler.BuiltAsync(built, cancellationToken);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ _logger.LogError(ex, "Error in orchestration context built handler {Handler}.", handler.GetType().Name);
+ }
+ }
+
+ if (context.CompletionContext != null && context.SystemMessageBuilder.Length > 0)
+ {
+ context.CompletionContext.SystemMessage = context.SystemMessageBuilder.ToString();
+ }
+
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ var systemMessage = context.CompletionContext?.SystemMessage;
+
+ if (!string.IsNullOrEmpty(systemMessage))
+ {
+ _logger.LogDebug(
+ "Composed system message ({Length} chars) for resource type '{ResourceType}': {SystemMessage}",
+ systemMessage.Length,
+ resource.GetType().Name,
+ systemMessage);
+ }
+ else
+ {
+ _logger.LogDebug("No system message composed for resource type '{ResourceType}'.", resource.GetType().Name);
+ }
+ }
+
+ return context;
+ }
+ }
+
+ private sealed class LegacyAICompletionContextBuilder
+ {
+ private readonly IEnumerable _handlers;
+ private readonly ILogger _logger;
+
+ public LegacyAICompletionContextBuilder(
+ IEnumerable handlers,
+ ILogger logger)
+ {
+ _handlers = handlers?.Reverse() ?? [];
+ _logger = logger;
+ }
+
+ public async ValueTask BuildAsync(
+ object resource,
+ Action configure = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(resource);
+
+ var context = new AICompletionContext();
+ var building = new AICompletionContextBuildingContext(resource, context);
+
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ await handler.BuildingAsync(building);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ _logger.LogError(ex, "Error in completion context building handler {Handler}.", handler.GetType().Name);
+ }
+ }
+
+ configure?.Invoke(context);
+
+ var built = new AICompletionContextBuiltContext(resource, context);
+
+ foreach (var handler in _handlers)
+ {
+ try
+ {
+ await handler.BuiltAsync(built);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ _logger.LogError(ex, "Error in completion context built handler {Handler}.", handler.GetType().Name);
+ }
+ }
+
+ return context;
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/CopilotOrchestratorPromptBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/CopilotOrchestratorPromptBenchmarks.cs
new file mode 100644
index 00000000..5136fd9f
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/CopilotOrchestratorPromptBenchmarks.cs
@@ -0,0 +1,504 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Copilot.Services;
+using CrestApps.Core.AI.Mcp;
+using CrestApps.Core.AI.Mcp.Models;
+using CrestApps.Core.AI.Models;
+using Cysharp.Text;
+using GitHub.Copilot;
+using Microsoft.Extensions.AI;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares the captured repeated reads with the production Copilot
+/// prompt builder.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class CopilotPromptHistoryBenchmarks
+{
+ private OrchestrationContext _context;
+
+ ///
+ /// Gets or sets the number of history messages.
+ ///
+ [Params(10, 100, 1_000, 10_000)]
+ public int HistoryCount { get; set; }
+
+ ///
+ /// Gets or sets the history content and role distribution.
+ ///
+ [Params(
+ CopilotPromptHistoryShape.SingleTextContent,
+ CopilotPromptHistoryShape.MultipleTextContents,
+ CopilotPromptHistoryShape.SparseRoles)]
+ public CopilotPromptHistoryShape Shape { get; set; }
+
+ ///
+ /// Creates stable prompt inputs and verifies exact output equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _context = new OrchestrationContext
+ {
+ UserMessage = "Summarize the release readiness and include the warning-free validation results.",
+ ConversationHistory = CreateHistory(HistoryCount, Shape),
+ };
+
+ var legacy = BuildPromptWithHistoryLegacy(_context);
+ var current = CopilotOrchestrator.BuildPromptWithHistory(_context);
+
+ if (!string.Equals(legacy, current, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("The production Copilot prompt changed the captured output.");
+ }
+ }
+
+ ///
+ /// Builds the prompt with the captured repeated aggregate-text reads.
+ ///
+ /// The complete Copilot prompt.
+ [Benchmark(Baseline = true)]
+ public string Legacy()
+ {
+ return BuildPromptWithHistoryLegacy(_context);
+ }
+
+ ///
+ /// Builds the prompt with the production implementation.
+ ///
+ /// The complete Copilot prompt.
+ [Benchmark]
+ public string Current()
+ {
+ return CopilotOrchestrator.BuildPromptWithHistory(_context);
+ }
+
+ ///
+ /// Preserves the original Copilot history-composition implementation as the benchmark baseline.
+ ///
+ /// The orchestration context.
+ /// The complete Copilot prompt.
+ private static string BuildPromptWithHistoryLegacy(OrchestrationContext context)
+ {
+ if (context.ConversationHistory is not { Count: > 0 })
+ {
+ return context.UserMessage;
+ }
+
+ using var sb = ZString.CreateStringBuilder();
+ sb.AppendLine("[Conversation History]");
+
+ foreach (var message in context.ConversationHistory)
+ {
+ if (string.IsNullOrEmpty(message.Text))
+ {
+ continue;
+ }
+
+ if (message.Role == ChatRole.User)
+ {
+ sb.Append("User: ");
+ }
+ else if (message.Role == ChatRole.Assistant)
+ {
+ sb.Append("Assistant: ");
+ }
+ else
+ {
+ continue;
+ }
+
+ sb.AppendLine(message.Text);
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("[Current Message]");
+ sb.Append(context.UserMessage);
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Creates a history with the requested content and role distribution.
+ ///
+ /// The number of messages to create.
+ /// The history shape.
+ /// The generated history.
+ private static List CreateHistory(
+ int historyCount,
+ CopilotPromptHistoryShape shape)
+ {
+ var history = new List(historyCount);
+
+ for (var index = 0; index < historyCount; index++)
+ {
+ history.Add(shape switch
+ {
+ CopilotPromptHistoryShape.SingleTextContent => CreateSingleTextMessage(index),
+ CopilotPromptHistoryShape.MultipleTextContents => CreateMultipleTextMessage(
+ index,
+ index % 2 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant),
+ CopilotPromptHistoryShape.SparseRoles => CreateSparseRoleMessage(index),
+ _ => throw new InvalidOperationException($"Unsupported history shape '{shape}'."),
+ });
+ }
+
+ return history;
+ }
+
+ ///
+ /// Creates a user or assistant message with one text-content item.
+ ///
+ /// The message index.
+ /// The generated message.
+ private static ChatMessage CreateSingleTextMessage(int index)
+ {
+ return new ChatMessage(
+ index % 2 == 0
+ ? ChatRole.User
+ : ChatRole.Assistant,
+ [new TextContent($"Message {index}: validate deployment, tests, packages, and release notes.")]);
+ }
+
+ ///
+ /// Creates a message with multiple text-content items and interleaved non-text content.
+ ///
+ /// The message index.
+ /// The message role.
+ /// The generated message.
+ private static ChatMessage CreateMultipleTextMessage(
+ int index,
+ ChatRole role)
+ {
+ return new ChatMessage
+ {
+ Role = role,
+ Contents =
+ [
+ new TextContent($"Message {index}: "),
+ new DataContent(new byte[] { 1, 2, 3, 4 }, "application/octet-stream"),
+ null,
+ new TextContent("validate deployment and tests; "),
+ new TextContent("include package and release-note status."),
+ ],
+ };
+ }
+
+ ///
+ /// Creates a history where one message in ten has a supported role.
+ ///
+ /// The message index.
+ /// The generated message.
+ private static ChatMessage CreateSparseRoleMessage(int index)
+ {
+ var role = (index % 10) switch
+ {
+ 0 => ChatRole.User,
+ 1 => ChatRole.Assistant,
+ 2 or 3 or 4 => ChatRole.System,
+ 5 or 6 or 7 => ChatRole.Tool,
+ _ => new ChatRole("observer"),
+ };
+
+ return CreateMultipleTextMessage(index, role);
+ }
+}
+
+///
+/// Defines the prompt-history distributions measured by the Copilot benchmark.
+///
+public enum CopilotPromptHistoryShape
+{
+ ///
+ /// Every message has a supported role and one text-content item.
+ ///
+ SingleTextContent,
+
+ ///
+ /// Every message has a supported role and multiple text-content items.
+ ///
+ MultipleTextContents,
+
+ ///
+ /// One message in ten has a supported role, with multiple text-content items on every message.
+ ///
+ SparseRoles,
+}
+
+///
+/// Compares the captured intermediate-description concatenation with the production MCP system-message
+/// composition.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class CopilotMcpSystemMessageBenchmarks
+{
+ private IReadOnlyCollection _connections;
+ private string _existingSystemMessage;
+
+ ///
+ /// Gets or sets the number of MCP connections.
+ ///
+ [Params(10, 100, 1_000)]
+ public int ConnectionCount { get; set; }
+
+ ///
+ /// Gets or sets whether an existing 8-KB system message is present.
+ ///
+ [Params(false, true)]
+ public bool HasExistingSystemMessage { get; set; }
+
+ ///
+ /// Creates stable connection inputs and verifies exact output equivalence.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _connections = CreateConnections(ConnectionCount);
+ _existingSystemMessage = HasExistingSystemMessage
+ ? new string('s', 8 * 1024)
+ : null;
+
+ var legacy = Legacy();
+ var current = Current();
+ var candidate = SingleBuilderCandidate();
+
+ if (!string.Equals(legacy, current, StringComparison.Ordinal) ||
+ !string.Equals(legacy, candidate, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("The production MCP description changed the captured output.");
+ }
+ }
+
+ ///
+ /// Builds the MCP system message with the captured intermediate description string.
+ ///
+ /// The complete system-message content.
+ [Benchmark(Baseline = true)]
+ public string Legacy()
+ {
+ var sessionConfig = CreateSessionConfig();
+
+ AppendMcpServerDescriptionLegacy(sessionConfig, _connections);
+
+ return sessionConfig.SystemMessage.Content;
+ }
+
+ ///
+ /// Builds the MCP system message with the production implementation.
+ ///
+ /// The complete system-message content.
+ [Benchmark]
+ public string Current()
+ {
+ var sessionConfig = CreateSessionConfig();
+
+ CopilotOrchestrator.AppendMcpServerDescription(sessionConfig, _connections);
+
+ return sessionConfig.SystemMessage.Content;
+ }
+
+ ///
+ /// Builds the MCP system message with the rejected single-builder candidate.
+ ///
+ /// The complete system-message content.
+ [Benchmark]
+ public string SingleBuilderCandidate()
+ {
+ var sessionConfig = CreateSessionConfig();
+
+ AppendMcpServerDescriptionSingleBuilderCandidate(sessionConfig, _connections);
+
+ return sessionConfig.SystemMessage.Content;
+ }
+
+ ///
+ /// Creates a new session configuration for one benchmark operation.
+ ///
+ /// The session configuration.
+ private SessionConfig CreateSessionConfig()
+ {
+ return new SessionConfig
+ {
+ SystemMessage = HasExistingSystemMessage
+ ? new SystemMessageConfig
+ {
+ Content = _existingSystemMessage,
+ }
+ : null,
+ };
+ }
+
+ ///
+ /// Preserves the original MCP system-message composition as the benchmark baseline.
+ ///
+ /// The Copilot session configuration.
+ /// The MCP connections to describe.
+ private static void AppendMcpServerDescriptionLegacy(
+ SessionConfig sessionConfig,
+ IReadOnlyCollection connections)
+ {
+ using var mcpDescription = ZString.CreateStringBuilder();
+ mcpDescription.AppendLine();
+ mcpDescription.AppendLine("[Available MCP Servers]");
+
+ foreach (var connection in connections)
+ {
+ mcpDescription.Append("- ");
+ mcpDescription.Append(connection.DisplayText ?? connection.ItemId);
+
+ if (connection.Source == McpConstants.TransportTypes.Sse)
+ {
+ if (connection.TryGet(out var sseMetadata) &&
+ sseMetadata.Endpoint is not null)
+ {
+ mcpDescription.Append(" (SSE: ");
+ mcpDescription.Append(sseMetadata.Endpoint);
+ mcpDescription.Append(')');
+ }
+ }
+ else if (connection.Source == McpConstants.TransportTypes.StdIo)
+ {
+ if (connection.TryGet(out var stdioMetadata) &&
+ !string.IsNullOrEmpty(stdioMetadata.Command))
+ {
+ mcpDescription.Append(" (StdIO: ");
+ mcpDescription.Append(stdioMetadata.Command);
+ mcpDescription.Append(')');
+ }
+ }
+
+ mcpDescription.AppendLine();
+ }
+
+ if (sessionConfig.SystemMessage is not null)
+ {
+ sessionConfig.SystemMessage.Content += mcpDescription.ToString();
+ }
+ else
+ {
+ sessionConfig.SystemMessage = new SystemMessageConfig
+ {
+ Content = mcpDescription.ToString(),
+ };
+ }
+ }
+
+ ///
+ /// Appends MCP descriptions after first copying existing content into the same builder.
+ ///
+ /// The Copilot session configuration.
+ /// The MCP connections to describe.
+ private static void AppendMcpServerDescriptionSingleBuilderCandidate(
+ SessionConfig sessionConfig,
+ IReadOnlyCollection connections)
+ {
+ var systemMessage = sessionConfig.SystemMessage;
+ using var systemMessageBuilder = ZString.CreateStringBuilder();
+
+ if (!string.IsNullOrEmpty(systemMessage?.Content))
+ {
+ systemMessageBuilder.Append(systemMessage.Content);
+ }
+
+ systemMessageBuilder.AppendLine();
+ systemMessageBuilder.AppendLine("[Available MCP Servers]");
+
+ foreach (var connection in connections)
+ {
+ systemMessageBuilder.Append("- ");
+ systemMessageBuilder.Append(connection.DisplayText ?? connection.ItemId);
+
+ if (connection.Source == McpConstants.TransportTypes.Sse)
+ {
+ if (connection.TryGet(out var sseMetadata) &&
+ sseMetadata.Endpoint is not null)
+ {
+ systemMessageBuilder.Append(" (SSE: ");
+ systemMessageBuilder.Append(sseMetadata.Endpoint);
+ systemMessageBuilder.Append(')');
+ }
+ }
+ else if (connection.Source == McpConstants.TransportTypes.StdIo)
+ {
+ if (connection.TryGet(out var stdioMetadata) &&
+ !string.IsNullOrEmpty(stdioMetadata.Command))
+ {
+ systemMessageBuilder.Append(" (StdIO: ");
+ systemMessageBuilder.Append(stdioMetadata.Command);
+ systemMessageBuilder.Append(')');
+ }
+ }
+
+ systemMessageBuilder.AppendLine();
+ }
+
+ if (systemMessage is not null)
+ {
+ systemMessage.Content = systemMessageBuilder.ToString();
+ }
+ else
+ {
+ sessionConfig.SystemMessage = new SystemMessageConfig
+ {
+ Content = systemMessageBuilder.ToString(),
+ };
+ }
+ }
+
+ ///
+ /// Creates mixed SSE, standard-input/output, and metadata-free MCP connections.
+ ///
+ /// The number of connections to create.
+ /// The generated connections.
+ private static McpConnection[] CreateConnections(int connectionCount)
+ {
+ var connections = new McpConnection[connectionCount];
+
+ for (var index = 0; index < connectionCount; index++)
+ {
+ var connection = new McpConnection
+ {
+ ItemId = $"connection-{index}",
+ DisplayText = index % 5 == 0
+ ? null
+ : $"Connection {index}",
+ };
+
+ switch (index % 3)
+ {
+ case 0:
+ connection.Source = McpConstants.TransportTypes.Sse;
+ connection.Put(new SseMcpConnectionMetadata
+ {
+ Endpoint = new Uri($"https://mcp.example.com/events/{index}"),
+ });
+ break;
+ case 1:
+ connection.Source = McpConstants.TransportTypes.StdIo;
+ connection.Put(new StdioMcpConnectionMetadata
+ {
+ Command = $"dotnet tool-{index}.dll --serve",
+ });
+ break;
+ default:
+ connection.Source = "custom";
+ break;
+ }
+
+ connections[index] = connection;
+ }
+
+ return connections;
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/CrestApps.Core.Benchmarks.csproj b/benchmarks/CrestApps.Core.Benchmarks/CrestApps.Core.Benchmarks.csproj
new file mode 100644
index 00000000..b8f6bd62
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/CrestApps.Core.Benchmarks.csproj
@@ -0,0 +1,33 @@
+
+
+
+ Exe
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DataExtractionMergeNamePartsBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionMergeNamePartsBenchmarks.cs
new file mode 100644
index 00000000..ee936a1e
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionMergeNamePartsBenchmarks.cs
@@ -0,0 +1,349 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Chat.Services;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures exact-equivalent name-part merging implementations across representative name shapes.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DataExtractionMergeNamePartsBenchmarks
+{
+ private const int OperationsPerInvocation = 1_000;
+ private string _existingValue;
+ private string _newValue;
+ private DataExtractionService.ExtractionFieldKind _resultFieldKind;
+
+ ///
+ /// Gets or sets the name shape used by the benchmark.
+ ///
+ [Params(
+ NameShape.ShortRealistic,
+ NameShape.PunctuationHeavy,
+ NameShape.Unicode,
+ NameShape.LongMultiPart)]
+ public NameShape Shape { get; set; }
+
+ ///
+ /// Configures representative inputs and verifies both experiments against the captured implementation.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ (_existingValue, _newValue, _resultFieldKind) = Shape switch
+ {
+ NameShape.ShortRealistic => (
+ "Ada Lovelace",
+ "Augusta",
+ DataExtractionService.ExtractionFieldKind.FirstName),
+ NameShape.PunctuationHeavy => (
+ "Dr. Jean-Luc O'Neill, Jr.",
+ "Smith-Jones (III)",
+ DataExtractionService.ExtractionFieldKind.LastName),
+ NameShape.Unicode => (
+ "李 小龍",
+ "김민수",
+ DataExtractionService.ExtractionFieldKind.FirstName),
+ NameShape.LongMultiPart => (
+ "María de los Ángeles del Río y Fernández von Habsburg-Lothringen al-Saud bin Abdulaziz",
+ "O'Connor-Smith",
+ DataExtractionService.ExtractionFieldKind.LastName),
+ _ => throw new InvalidOperationException($"Unsupported name shape '{Shape}'."),
+ };
+
+ var expected = MergeLegacy(_existingValue, _newValue, _resultFieldKind);
+
+ if (!string.Equals(expected, MergeCurrent(_existingValue, _newValue, _resultFieldKind), StringComparison.Ordinal) ||
+ !string.Equals(expected, MergeManualSpan(_existingValue, _newValue, _resultFieldKind), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"Name merging experiment changed output for '{Shape}'.");
+ }
+ }
+
+ ///
+ /// Runs the captured Split, LINQ, collection-expression, and Join implementation repeatedly.
+ ///
+ /// The combined output length.
+ [Benchmark(Baseline = true, OperationsPerInvoke = OperationsPerInvocation)]
+ public int MergeLegacyAtScale()
+ {
+ var length = 0;
+
+ for (var index = 0; index < OperationsPerInvocation; index++)
+ {
+ length += MergeLegacy(_existingValue, _newValue, _resultFieldKind).Length;
+ }
+
+ return length;
+ }
+
+ ///
+ /// Runs the simpler Split implementation that replaces the relevant array element before Join.
+ ///
+ /// The combined output length.
+ [Benchmark(OperationsPerInvoke = OperationsPerInvocation)]
+ public int MergeCurrentAtScale()
+ {
+ var length = 0;
+
+ for (var index = 0; index < OperationsPerInvocation; index++)
+ {
+ length += MergeCurrent(_existingValue, _newValue, _resultFieldKind).Length;
+ }
+
+ return length;
+ }
+
+ ///
+ /// Runs the allocation-reducing manual literal-space scanner repeatedly.
+ ///
+ /// The combined output length.
+ [Benchmark(OperationsPerInvoke = OperationsPerInvocation)]
+ public int MergeManualSpanExperimentAtScale()
+ {
+ var length = 0;
+
+ for (var index = 0; index < OperationsPerInvocation; index++)
+ {
+ length += MergeManualSpan(_existingValue, _newValue, _resultFieldKind).Length;
+ }
+
+ return length;
+ }
+
+ ///
+ /// Captures the production implementation before local name-merging optimization.
+ ///
+ /// The existing extracted value.
+ /// The incoming name part.
+ /// The incoming field kind.
+ /// The merged name.
+ private static string MergeLegacy(
+ string existingValue,
+ string newValue,
+ DataExtractionService.ExtractionFieldKind resultFieldKind)
+ {
+ if (string.IsNullOrWhiteSpace(newValue))
+ {
+ return existingValue;
+ }
+
+ if (string.IsNullOrWhiteSpace(existingValue))
+ {
+ return newValue.Trim();
+ }
+
+ var existingParts = existingValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ var incomingValue = newValue.Trim();
+
+ if (resultFieldKind == DataExtractionService.ExtractionFieldKind.FirstName)
+ {
+ return existingParts.Length > 1
+ ? string.Join(' ', [incomingValue, .. existingParts.Skip(1)])
+ : incomingValue;
+ }
+
+ if (existingParts.Length > 1)
+ {
+ return string.Join(' ', [.. existingParts.Take(existingParts.Length - 1), incomingValue]);
+ }
+
+ return string.Concat(existingParts[0], " ", incomingValue);
+ }
+
+ ///
+ /// Reuses the array produced by Split instead of projecting its elements into a second array.
+ ///
+ /// The existing extracted value.
+ /// The incoming name part.
+ /// The incoming field kind.
+ /// The merged name.
+ private static string MergeCurrent(
+ string existingValue,
+ string newValue,
+ DataExtractionService.ExtractionFieldKind resultFieldKind)
+ {
+ if (string.IsNullOrWhiteSpace(newValue))
+ {
+ return existingValue;
+ }
+
+ if (string.IsNullOrWhiteSpace(existingValue))
+ {
+ return newValue.Trim();
+ }
+
+ var existingParts = existingValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ var incomingValue = newValue.Trim();
+
+ if (resultFieldKind == DataExtractionService.ExtractionFieldKind.FirstName)
+ {
+ if (existingParts.Length == 1)
+ {
+ return incomingValue;
+ }
+
+ existingParts[0] = incomingValue;
+
+ return string.Join(' ', existingParts);
+ }
+
+ if (existingParts.Length > 1)
+ {
+ existingParts[^1] = incomingValue;
+
+ return string.Join(' ', existingParts);
+ }
+
+ return string.Concat(existingParts[0], " ", incomingValue);
+ }
+
+ ///
+ /// Scans literal-space-delimited parts without allocating an array or individual substrings.
+ ///
+ /// The existing extracted value.
+ /// The incoming name part.
+ /// The incoming field kind.
+ /// The merged name.
+ private static string MergeManualSpan(
+ string existingValue,
+ string newValue,
+ DataExtractionService.ExtractionFieldKind resultFieldKind)
+ {
+ if (string.IsNullOrWhiteSpace(newValue))
+ {
+ return existingValue;
+ }
+
+ if (string.IsNullOrWhiteSpace(existingValue))
+ {
+ return newValue.Trim();
+ }
+
+ var incomingValue = newValue.Trim();
+ var position = 0;
+ _ = TryReadNextNamePart(existingValue, ref position, out var firstStart, out var firstLength);
+
+ if (resultFieldKind == DataExtractionService.ExtractionFieldKind.FirstName)
+ {
+ if (!TryReadNextNamePart(existingValue, ref position, out var partStart, out var partLength))
+ {
+ return incomingValue;
+ }
+
+ var builder = new StringBuilder(existingValue.Length + incomingValue.Length);
+ builder.Append(incomingValue);
+ builder.Append(' ');
+ builder.Append(existingValue, partStart, partLength);
+
+ while (TryReadNextNamePart(existingValue, ref position, out partStart, out partLength))
+ {
+ builder.Append(' ');
+ builder.Append(existingValue, partStart, partLength);
+ }
+
+ return builder.ToString();
+ }
+
+ if (!TryReadNextNamePart(existingValue, ref position, out var pendingStart, out var pendingLength))
+ {
+ return string.Concat(existingValue.AsSpan(firstStart, firstLength), " ", incomingValue);
+ }
+
+ var prefixBuilder = new StringBuilder(existingValue.Length + incomingValue.Length);
+ prefixBuilder.Append(existingValue, firstStart, firstLength);
+
+ while (TryReadNextNamePart(existingValue, ref position, out var partStart, out var partLength))
+ {
+ prefixBuilder.Append(' ');
+ prefixBuilder.Append(existingValue, pendingStart, pendingLength);
+ pendingStart = partStart;
+ pendingLength = partLength;
+ }
+
+ prefixBuilder.Append(' ');
+ prefixBuilder.Append(incomingValue);
+
+ return prefixBuilder.ToString();
+ }
+
+ ///
+ /// Reads the next non-empty part using the same literal-space split and Unicode trimming rules as String.Split.
+ ///
+ /// The value being scanned.
+ /// The next scan position.
+ /// The trimmed part start.
+ /// The trimmed part length.
+ /// when a non-empty part was found.
+ private static bool TryReadNextNamePart(
+ string value,
+ ref int position,
+ out int partStart,
+ out int partLength)
+ {
+ while (position <= value.Length)
+ {
+ var segmentStart = position;
+ var separatorOffset = value.AsSpan(position).IndexOf(' ');
+ var segmentEnd = separatorOffset < 0
+ ? value.Length
+ : position + separatorOffset;
+ position = separatorOffset < 0
+ ? value.Length + 1
+ : segmentEnd + 1;
+
+ while (segmentStart < segmentEnd && char.IsWhiteSpace(value[segmentStart]))
+ {
+ segmentStart++;
+ }
+
+ while (segmentEnd > segmentStart && char.IsWhiteSpace(value[segmentEnd - 1]))
+ {
+ segmentEnd--;
+ }
+
+ if (segmentEnd > segmentStart)
+ {
+ partStart = segmentStart;
+ partLength = segmentEnd - segmentStart;
+
+ return true;
+ }
+ }
+
+ partStart = 0;
+ partLength = 0;
+
+ return false;
+ }
+
+ ///
+ /// Identifies the representative name input shape.
+ ///
+ public enum NameShape
+ {
+ ///
+ /// A common two-part Latin name.
+ ///
+ ShortRealistic,
+
+ ///
+ /// A title and punctuation-heavy multi-part name.
+ ///
+ PunctuationHeavy,
+
+ ///
+ /// A non-Latin Unicode name.
+ ///
+ Unicode,
+
+ ///
+ /// A long name containing many literal-space-delimited parts.
+ ///
+ LongMultiPart,
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DataExtractionPromptProjectionBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionPromptProjectionBenchmarks.cs
new file mode 100644
index 00000000..bb0c5648
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionPromptProjectionBenchmarks.cs
@@ -0,0 +1,390 @@
+using System.Text.Json;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Models;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures the local field and current-state projections used to build data-extraction prompt arguments.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DataExtractionPromptProjectionBenchmarks
+{
+ private const string LastUserMessage = "My updated details are Ada Lovelace, ada@example.com, and +1 425 555 0100.";
+ private const string LastAssistantMessage = "Please provide your full name, email address, and preferred phone number.";
+ private List _fields;
+ private AIChatSession _session;
+
+ ///
+ /// Gets or sets the number of configured fields and current-state entries.
+ ///
+ [Params(10, 100, 1_000)]
+ public int EntryCount { get; set; }
+
+ ///
+ /// Gets or sets whether the current extracted state is sparse or dense.
+ ///
+ [Params(StateDensity.Sparse, StateDensity.Dense)]
+ public StateDensity Density { get; set; }
+
+ ///
+ /// Creates realistic fields and extracted state, then verifies every projection experiment.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _fields = new List(EntryCount);
+ _session = new AIChatSession();
+
+ for (var index = 0; index < EntryCount; index++)
+ {
+ _fields.Add(CreateEntry(index));
+
+ var state = CreateState(index);
+ _session.ExtractedData.Add($"state_{index:D4}", state);
+ }
+
+ var expected = JsonSerializer.Serialize(ProjectLegacy());
+
+ if (!string.Equals(expected, JsonSerializer.Serialize(ProjectFieldsWithConvertAll()), StringComparison.Ordinal) ||
+ !string.Equals(expected, JsonSerializer.Serialize(ProjectCurrent()), StringComparison.Ordinal) ||
+ !string.Equals(expected, JsonSerializer.Serialize(ProjectSinglePassStateExperiment()), StringComparison.Ordinal) ||
+ !string.Equals(expected, JsonSerializer.Serialize(ProjectWithPreSizedStateExperiment()), StringComparison.Ordinal) ||
+ !string.Equals(expected, JsonSerializer.Serialize(ProjectWithCountedStateExperiment()), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Prompt projection experiment changed output for {EntryCount} '{Density}' entries.");
+ }
+ }
+
+ ///
+ /// Projects prompt arguments using the captured LINQ Select, Where, and ToList implementation.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark(Baseline = true)]
+ public Dictionary ProjectLegacy()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.Select(field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }).ToList(),
+ ["currentState"] = _session.ExtractedData?
+ .Where(entry => entry.Value?.Values.Count > 0)
+ .Select(entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ })
+ .ToList() ?? [],
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Projects prompt arguments using the unchanged production LINQ implementation.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark]
+ public Dictionary ProjectCurrent()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.Select(field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }).ToList(),
+ ["currentState"] = _session.ExtractedData?
+ .Where(entry => entry.Value?.Values.Count > 0)
+ .Select(entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ })
+ .ToList() ?? [],
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Replaces only the unfiltered field projection with List.ConvertAll.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark]
+ public Dictionary ProjectFieldsWithConvertAll()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.ConvertAll(static field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }),
+ ["currentState"] = _session.ExtractedData?
+ .Where(entry => entry.Value?.Values.Count > 0)
+ .Select(entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ })
+ .ToList() ?? [],
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Uses ConvertAll for fields and a single-pass, non-pre-sized loop for filtered current state.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark]
+ public Dictionary ProjectSinglePassStateExperiment()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.ConvertAll(static field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }),
+ ["currentState"] = SelectPopulatedState(
+ _session.ExtractedData,
+ 0,
+ static entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ }),
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Tests pre-sizing filtered current state to the full dictionary count.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark]
+ public Dictionary ProjectWithPreSizedStateExperiment()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.ConvertAll(static field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }),
+ ["currentState"] = SelectPopulatedState(
+ _session.ExtractedData,
+ _session.ExtractedData?.Count ?? 0,
+ static entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ }),
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Tests an exact-count pass before allocating and filling filtered current state.
+ ///
+ /// The projected prompt argument dictionary.
+ [Benchmark]
+ public Dictionary ProjectWithCountedStateExperiment()
+ {
+ var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["fields"] = _fields.ConvertAll(static field => new
+ {
+ field.Name,
+ field.Description,
+ field.AllowMultipleValues,
+ field.IsUpdatable,
+ }),
+ ["currentState"] = SelectPopulatedStateWithCount(
+ _session.ExtractedData,
+ static entry => new
+ {
+ Name = entry.Key,
+ Values = entry.Value.Values,
+ }),
+ ["lastUserMessage"] = LastUserMessage,
+ };
+ arguments["lastAssistantMessage"] = LastAssistantMessage;
+
+ return arguments;
+ }
+
+ ///
+ /// Creates a realistic extraction entry containing aliases and examples.
+ ///
+ /// The entry index.
+ /// The configured entry.
+ private static DataExtractionEntry CreateEntry(int index)
+ {
+ var (name, description) = (index % 5) switch
+ {
+ 0 => (
+ "customer_name",
+ "Customer full name. Aliases: fullName, display name. Examples: Ada Lovelace; 李 小龍."),
+ 1 => (
+ "email_address",
+ "Primary email. Aliases: email, e-mail. Examples: ada@example.com; support+vip@example.org."),
+ 2 => (
+ "phone_number",
+ "Preferred phone. Aliases: phone, mobile, telephone. Examples: +1 425 555 0100; (702) 555-0199."),
+ 3 => (
+ $"account_reference_{index % 17}",
+ "Customer account reference. Aliases: account ID, reference. Examples: ACCT-1042; EU/2026/009."),
+ _ => (
+ $"custom_field_{index}",
+ "Free-form profile value. Aliases: custom value. Examples: Gold; Montréal; 東京."),
+ };
+
+ return new DataExtractionEntry
+ {
+ Name = name,
+ Description = description,
+ AllowMultipleValues = index % 3 == 0,
+ IsUpdatable = index % 2 == 0,
+ };
+ }
+
+ ///
+ /// Creates extracted state at the configured density, including null and empty states.
+ ///
+ /// The state index.
+ /// The extracted field state.
+ private ExtractedFieldState CreateState(int index)
+ {
+ if (index % 41 == 0)
+ {
+ return null;
+ }
+
+ var isPopulated = Density == StateDensity.Dense
+ ? index % 10 != 0
+ : index % 10 == 0;
+
+ if (!isPopulated)
+ {
+ return new ExtractedFieldState();
+ }
+
+ return new ExtractedFieldState
+ {
+ Values = (index % 3) switch
+ {
+ 0 => ["Ada Lovelace", "李 小龍"],
+ 1 => ["ada@example.com"],
+ _ => ["+1 425 555 0100", "(702) 555-0199", null],
+ },
+ };
+ }
+
+ ///
+ /// Projects populated state entries in one pass with the requested initial capacity.
+ ///
+ /// The anonymous projection type.
+ /// The extracted state dictionary.
+ /// The initial result capacity.
+ /// The projection selector.
+ /// The projected populated state.
+ private static List SelectPopulatedState(
+ IReadOnlyDictionary source,
+ int capacity,
+ Func, TResult> selector)
+ {
+ if (source is null)
+ {
+ return [];
+ }
+
+ var results = new List(capacity);
+
+ foreach (var entry in source)
+ {
+ if (entry.Value?.Values.Count > 0)
+ {
+ results.Add(selector(entry));
+ }
+ }
+
+ return results;
+ }
+
+ ///
+ /// Counts populated state entries before allocating and filling the exact-sized result list.
+ ///
+ /// The anonymous projection type.
+ /// The extracted state dictionary.
+ /// The projection selector.
+ /// The projected populated state.
+ private static List SelectPopulatedStateWithCount(
+ IReadOnlyDictionary source,
+ Func, TResult> selector)
+ {
+ if (source is null)
+ {
+ return [];
+ }
+
+ var count = 0;
+
+ foreach (var entry in source)
+ {
+ if (entry.Value?.Values.Count > 0)
+ {
+ count++;
+ }
+ }
+
+ return SelectPopulatedState(source, count, selector);
+ }
+
+ ///
+ /// Identifies the proportion of current-state entries containing values.
+ ///
+ public enum StateDensity
+ {
+ ///
+ /// Approximately ten percent of entries contain extracted values.
+ ///
+ Sparse,
+
+ ///
+ /// Approximately ninety percent of entries contain extracted values.
+ ///
+ Dense,
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DataExtractionServiceBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionServiceBenchmarks.cs
new file mode 100644
index 00000000..6c1a3d0a
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DataExtractionServiceBenchmarks.cs
@@ -0,0 +1,258 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Chat.Services;
+using CrestApps.Core.AI.Models;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures matching extracted result names to configured extraction entries.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DataExtractionMatchingBenchmarks
+{
+ private List _entries;
+ private string[] _resultNames;
+
+ ///
+ /// Gets or sets the number of configured entries and extracted results.
+ ///
+ [Params(10, 100)]
+ public int EntryCount { get; set; }
+
+ ///
+ /// Creates configured entries and a mixed set of direct, normalized, semantic, and unknown results.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _entries = new List(EntryCount);
+
+ for (var index = 0; index < EntryCount - 2; index++)
+ {
+ _entries.Add(new DataExtractionEntry
+ {
+ Name = $"field_{index}",
+ Description = $"Configured field {index}",
+ });
+ }
+
+ _entries.Add(new DataExtractionEntry
+ {
+ Name = "customer_name",
+ Description = "The customer's full name.",
+ });
+ _entries.Add(new DataExtractionEntry
+ {
+ Name = "customer_phone",
+ Description = "The customer's phone number.",
+ });
+
+ _resultNames = new string[EntryCount];
+
+ for (var index = 0; index < EntryCount; index++)
+ {
+ _resultNames[index] = (index % 4) switch
+ {
+ 0 => $"field_{index % (EntryCount - 2)}",
+ 1 => $"field-{index % (EntryCount - 2)}",
+ 2 => index % 8 == 2 ? "firstName" : "phoneNumber",
+ _ => $"unknown-{index}",
+ };
+ }
+ }
+
+ ///
+ /// Matches every result with repeated configured-entry scans and normalization.
+ ///
+ /// The number of matched results.
+ [Benchmark(Baseline = true)]
+ public int MatchLegacy()
+ {
+ var count = 0;
+
+ foreach (var resultName in _resultNames)
+ {
+ if (FindLegacy(resultName) != null)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// Builds one per-call index and reuses it for every extracted result.
+ ///
+ /// The number of matched results.
+ [Benchmark]
+ public int MatchOptimized()
+ {
+ var index = new DataExtractionService.ExtractionEntryIndex(_entries);
+ var count = 0;
+
+ foreach (var resultName in _resultNames)
+ {
+ if (index.Find(resultName) != null)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private LegacyMatch FindLegacy(string resultName)
+ {
+ var directMatch = _entries.FirstOrDefault(entry =>
+ string.Equals(entry.Name, resultName, StringComparison.OrdinalIgnoreCase));
+
+ if (directMatch != null)
+ {
+ return new LegacyMatch(directMatch, Classify(resultName), false);
+ }
+
+ var normalizedResultName = Normalize(resultName);
+
+ if (string.IsNullOrEmpty(normalizedResultName))
+ {
+ return null;
+ }
+
+ var normalizedMatch = _entries.FirstOrDefault(entry =>
+ string.Equals(Normalize(entry.Name), normalizedResultName, StringComparison.OrdinalIgnoreCase));
+
+ if (normalizedMatch != null)
+ {
+ return new LegacyMatch(normalizedMatch, Classify(resultName), false);
+ }
+
+ var resultKind = Classify(resultName);
+
+ if (resultKind == FieldKind.Unknown)
+ {
+ return null;
+ }
+
+ var semanticMatch = _entries.FirstOrDefault(entry => IsSemanticMatch(entry, resultKind));
+
+ return semanticMatch == null
+ ? null
+ : new LegacyMatch(semanticMatch, resultKind, true);
+ }
+
+ private static bool IsSemanticMatch(DataExtractionEntry entry, FieldKind resultKind)
+ {
+ var entryKind = Classify(entry?.Name, entry?.Description);
+
+ if (resultKind == FieldKind.PhoneNumber)
+ {
+ return entryKind == FieldKind.PhoneNumber;
+ }
+
+ if (resultKind is FieldKind.FirstName or FieldKind.LastName or FieldKind.FullName)
+ {
+ return entryKind == FieldKind.FullName;
+ }
+
+ return false;
+ }
+
+ private static string Normalize(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return null;
+ }
+
+ var builder = new StringBuilder(name.Length);
+
+ foreach (var character in name)
+ {
+ if (char.IsLetterOrDigit(character))
+ {
+ builder.Append(char.ToLowerInvariant(character));
+ }
+ }
+
+ return builder.Length == 0
+ ? null
+ : builder.ToString();
+ }
+
+ private static FieldKind Classify(
+ string name,
+ string description = null)
+ {
+ return ClassifyNormalized(Normalize(name), Normalize(description));
+ }
+
+ private static FieldKind ClassifyNormalized(
+ string normalizedName,
+ string normalizedDescription)
+ {
+ if (ContainsAny(normalizedName, normalizedDescription, "phone", "phonenumber", "telephone", "mobile", "cell"))
+ {
+ return FieldKind.PhoneNumber;
+ }
+
+ if (ContainsAny(normalizedName, normalizedDescription, "firstname", "givenname"))
+ {
+ return FieldKind.FirstName;
+ }
+
+ if (ContainsAny(normalizedName, normalizedDescription, "lastname", "surname", "familyname"))
+ {
+ return FieldKind.LastName;
+ }
+
+ if (ContainsAny(normalizedName, normalizedDescription, "fullname", "customername"))
+ {
+ return FieldKind.FullName;
+ }
+
+ if (!string.IsNullOrEmpty(normalizedDescription) &&
+ normalizedDescription.Contains("firstname", StringComparison.Ordinal) &&
+ normalizedDescription.Contains("lastname", StringComparison.Ordinal))
+ {
+ return FieldKind.FullName;
+ }
+
+ return FieldKind.Unknown;
+ }
+
+ private static bool ContainsAny(
+ string normalizedName,
+ string normalizedDescription,
+ params string[] candidates)
+ {
+ foreach (var candidate in candidates)
+ {
+ if ((!string.IsNullOrEmpty(normalizedName) && normalizedName.Contains(candidate, StringComparison.Ordinal)) ||
+ (!string.IsNullOrEmpty(normalizedDescription) && normalizedDescription.Contains(candidate, StringComparison.Ordinal)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private enum FieldKind
+ {
+ Unknown,
+ FullName,
+ FirstName,
+ LastName,
+ PhoneNumber,
+ }
+
+ private sealed record LegacyMatch(
+ DataExtractionEntry Entry,
+ FieldKind ResultFieldKind,
+ bool IsSemanticAlias);
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DataSourceAzureAISearchDocumentIdFilterBuilderBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DataSourceAzureAISearchDocumentIdFilterBuilderBenchmarks.cs
new file mode 100644
index 00000000..c9f6f334
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DataSourceAzureAISearchDocumentIdFilterBuilderBenchmarks.cs
@@ -0,0 +1,69 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.Azure.AISearch.Services;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Compares legacy and current Azure AI Search document identifier filter preparation.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DataSourceAzureAISearchDocumentIdFilterBuilderBenchmarks
+{
+ private const string KeyFieldName = "documentId";
+
+ private string[] _documentIds;
+
+ ///
+ /// Gets or sets the number of valid document identifiers included in each filter.
+ ///
+ [Params(1, 10, 100, 1000)]
+ public int DocumentIdCount { get; set; }
+
+ ///
+ /// Creates document identifiers that exercise filtering and apostrophe escaping.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _documentIds = new string[DocumentIdCount + 2];
+
+ for (var index = 0; index < DocumentIdCount; index++)
+ {
+ _documentIds[index] = index % 10 == 0
+ ? $"document-'quoted'-{index}"
+ : $"document-{index}";
+ }
+
+ _documentIds[^2] = null;
+ _documentIds[^1] = string.Empty;
+ }
+
+ ///
+ /// Filters identifiers and builds the OData filter with the original LINQ implementation.
+ ///
+ /// The OData filter.
+ [Benchmark(Baseline = true)]
+ public string PrepareLegacy()
+ {
+ var idList = _documentIds.Where(id => !string.IsNullOrEmpty(id)).ToList();
+
+ return string.Join(
+ " or ",
+ idList.Select(id => $"{KeyFieldName} eq '{id.Replace("'", "''")}'"));
+ }
+
+ ///
+ /// Filters identifiers and builds the OData filter with the production implementation.
+ ///
+ /// The OData filter.
+ [Benchmark]
+ public string PrepareCurrent()
+ {
+ var idList = DataSourceAzureAISearchDocumentIdFilterBuilder.FilterDocumentIds(_documentIds);
+
+ return DataSourceAzureAISearchDocumentIdFilterBuilder.BuildFilter(idList, KeyFieldName);
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DefaultAIChatSessionExtractedDataRecorderBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DefaultAIChatSessionExtractedDataRecorderBenchmarks.cs
new file mode 100644
index 00000000..31ba717b
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DefaultAIChatSessionExtractedDataRecorderBenchmarks.cs
@@ -0,0 +1,382 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI.Chat;
+using CrestApps.Core.AI.Chat.Services;
+using CrestApps.Core.AI.Models;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Defines extracted-data map densities used by the recorder benchmarks.
+///
+public enum ExtractedDataMapDensity
+{
+ ///
+ /// Every source field has retained values.
+ ///
+ Dense,
+
+ ///
+ /// One percent of source fields have retained values.
+ ///
+ MostlyEmpty,
+
+ ///
+ /// No source fields have retained values.
+ ///
+ AllEmpty,
+}
+
+///
+/// Measures the captured LINQ snapshot construction against the current extracted-data recorder.
+/// The in-memory store excludes persistence so the benchmark isolates snapshot construction.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DefaultAIChatSessionExtractedDataRecorderBenchmarks
+{
+ private AIProfile _profile;
+ private AIChatSession _session;
+ private LegacyAIChatSessionExtractedDataRecorder _legacyRecorder;
+ private DefaultAIChatSessionExtractedDataRecorder _currentRecorder;
+ private NoOpExtractedDataStore _legacyStore;
+ private NoOpExtractedDataStore _currentStore;
+
+ ///
+ /// Gets or sets the number of source extracted-data fields.
+ ///
+ [Params(1000, 10000)]
+ public int FieldCount { get; set; }
+
+ ///
+ /// Gets or sets the density of retained extracted-data fields.
+ ///
+ [Params(
+ ExtractedDataMapDensity.Dense,
+ ExtractedDataMapDensity.MostlyEmpty,
+ ExtractedDataMapDensity.AllEmpty)]
+ public ExtractedDataMapDensity Density { get; set; }
+
+ ///
+ /// Creates descending mixed-case keys and retained value lists for the selected density.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ _profile = new AIProfile
+ {
+ ItemId = "profile-1",
+ };
+ _session = new AIChatSession
+ {
+ SessionId = "session-1",
+ CreatedUtc = new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc),
+ ClosedAtUtc = new DateTime(2026, 5, 1, 12, 5, 0, DateTimeKind.Utc),
+ ExtractedData = new Dictionary(FieldCount, StringComparer.Ordinal),
+ };
+
+ for (var index = FieldCount - 1; index >= 0; index--)
+ {
+ var key = (index % 3) switch
+ {
+ 0 => $"FIELD-{index:D5}",
+ 1 => $"Field-{index:D5}",
+ _ => $"field-{index:D5}",
+ };
+ var values = new List();
+
+ if (ShouldRetain(index))
+ {
+ values.Add(index % 7 == 3
+ ? null
+ : $"value-{index:D5}-0");
+ values.Add($"value-{index:D5}-1");
+ values.Add($"value-{index:D5}-2");
+ }
+
+ _session.ExtractedData.Add(
+ key,
+ new ExtractedFieldState
+ {
+ Values = values,
+ });
+ }
+
+ var timeProvider = new FixedTimeProvider(
+ new DateTimeOffset(2026, 5, 1, 12, 6, 0, TimeSpan.Zero));
+ _legacyStore = new NoOpExtractedDataStore();
+ _currentStore = new NoOpExtractedDataStore();
+ _legacyRecorder = new LegacyAIChatSessionExtractedDataRecorder(_legacyStore, timeProvider);
+ _currentRecorder = new DefaultAIChatSessionExtractedDataRecorder(_currentStore, timeProvider);
+
+ _legacyRecorder.RecordExtractedDataAsync(_profile, _session).GetAwaiter().GetResult();
+ _currentRecorder.RecordExtractedDataAsync(_profile, _session).GetAwaiter().GetResult();
+ EnsureEquivalent(_legacyStore, _currentStore, _session);
+ _legacyStore.Reset();
+ _currentStore.Reset();
+ }
+
+ ///
+ /// Records a snapshot with the production LINQ implementation captured before optimization.
+ ///
+ /// A task representing the recording operation.
+ [Benchmark(Baseline = true)]
+ public Task RecordWithLegacyLinq()
+ {
+ return _legacyRecorder.RecordExtractedDataAsync(_profile, _session);
+ }
+
+ ///
+ /// Records a snapshot with the current production implementation.
+ ///
+ /// A task representing the recording operation.
+ [Benchmark]
+ public Task RecordWithCurrentImplementation()
+ {
+ return _currentRecorder.RecordExtractedDataAsync(_profile, _session);
+ }
+
+ ///
+ /// Determines whether the field at the specified index should contain retained values.
+ ///
+ /// The source field index.
+ /// when the field should be retained; otherwise, .
+ private bool ShouldRetain(int index)
+ {
+ return Density switch
+ {
+ ExtractedDataMapDensity.Dense => true,
+ ExtractedDataMapDensity.MostlyEmpty => index % 100 == 0,
+ _ => false,
+ };
+ }
+
+ ///
+ /// Verifies both implementations produce equivalent calls, metadata, ordering, comparer, and values.
+ ///
+ /// The store used by the captured implementation.
+ /// The store used by the current implementation.
+ /// The source session used to verify detached lists.
+ private static void EnsureEquivalent(
+ NoOpExtractedDataStore legacyStore,
+ NoOpExtractedDataStore currentStore,
+ AIChatSession session)
+ {
+ if (legacyStore.SaveCount != currentStore.SaveCount ||
+ legacyStore.DeleteCount != currentStore.DeleteCount)
+ {
+ throw new InvalidOperationException("Recorder implementations invoked different store operations.");
+ }
+
+ var legacy = legacyStore.LastSavedRecord;
+ var current = currentStore.LastSavedRecord;
+
+ if (legacy is null || current is null)
+ {
+ if (legacy is not null ||
+ current is not null ||
+ legacyStore.SaveCount != 0 ||
+ legacyStore.DeleteCount != 1)
+ {
+ throw new InvalidOperationException("Recorder implementations returned different delete results.");
+ }
+
+ return;
+ }
+
+ if (legacy.ItemId != current.ItemId ||
+ legacy.SessionId != current.SessionId ||
+ legacy.ProfileId != current.ProfileId ||
+ legacy.SessionStartedUtc != current.SessionStartedUtc ||
+ legacy.SessionEndedUtc != current.SessionEndedUtc ||
+ legacy.UpdatedUtc != current.UpdatedUtc ||
+ !ReferenceEquals(legacy.Values.Comparer, current.Values.Comparer) ||
+ !legacy.Values.Keys.SequenceEqual(current.Values.Keys))
+ {
+ throw new InvalidOperationException("Recorder implementations returned different snapshots.");
+ }
+
+ foreach (var pair in legacy.Values)
+ {
+ if (!current.Values.TryGetValue(pair.Key, out var currentValues) ||
+ !pair.Value.SequenceEqual(currentValues) ||
+ ReferenceEquals(pair.Value, session.ExtractedData[pair.Key].Values) ||
+ ReferenceEquals(currentValues, session.ExtractedData[pair.Key].Values))
+ {
+ throw new InvalidOperationException("Recorder implementations returned different field values.");
+ }
+ }
+ }
+
+ ///
+ /// Captures the pre-optimization production recorder implementation.
+ ///
+ private sealed class LegacyAIChatSessionExtractedDataRecorder
+ {
+ private readonly IAIChatSessionExtractedDataStore _store;
+ private readonly TimeProvider _timeProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The in-memory store.
+ /// The fixed time provider.
+ public LegacyAIChatSessionExtractedDataRecorder(
+ IAIChatSessionExtractedDataStore store,
+ TimeProvider timeProvider)
+ {
+ _store = store;
+ _timeProvider = timeProvider;
+ }
+
+ ///
+ /// Records the extracted-data snapshot using the captured LINQ implementation.
+ ///
+ /// The AI profile.
+ /// The chat session.
+ /// The cancellation token.
+ /// A task representing the recording operation.
+ public async Task RecordExtractedDataAsync(
+ AIProfile profile,
+ AIChatSession session,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(profile);
+ ArgumentNullException.ThrowIfNull(session);
+ ArgumentException.ThrowIfNullOrWhiteSpace(session.SessionId);
+
+ var values = session.ExtractedData
+ .Where(pair => pair.Value.Values.Count > 0)
+ .ToDictionary(
+ pair => pair.Key,
+ pair => pair.Value.Values.ToList(),
+ StringComparer.OrdinalIgnoreCase);
+
+ if (values.Count == 0)
+ {
+ await _store.DeleteAsync(session.SessionId, cancellationToken);
+
+ return;
+ }
+
+ await _store.SaveAsync(
+ new AIChatSessionExtractedDataRecord
+ {
+ ItemId = session.SessionId,
+ SessionId = session.SessionId,
+ ProfileId = profile.ItemId,
+ SessionStartedUtc = session.CreatedUtc,
+ SessionEndedUtc = session.ClosedAtUtc,
+ UpdatedUtc = _timeProvider.GetUtcNow().UtcDateTime,
+ Values = values,
+ },
+ cancellationToken);
+ }
+ }
+
+ ///
+ /// Provides a fixed UTC timestamp without adding clock variability to the benchmark.
+ ///
+ private sealed class FixedTimeProvider : TimeProvider
+ {
+ private readonly DateTimeOffset _utcNow;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The fixed current time.
+ public FixedTimeProvider(DateTimeOffset utcNow)
+ {
+ _utcNow = utcNow;
+ }
+
+ ///
+ /// Gets the fixed UTC timestamp.
+ ///
+ /// The fixed timestamp.
+ public override DateTimeOffset GetUtcNow()
+ {
+ return _utcNow;
+ }
+ }
+
+ ///
+ /// Captures store calls and the last saved record without persistence or I/O.
+ ///
+ private sealed class NoOpExtractedDataStore : IAIChatSessionExtractedDataStore
+ {
+ ///
+ /// Gets the last snapshot passed to .
+ ///
+ public AIChatSessionExtractedDataRecord LastSavedRecord { get; private set; }
+
+ ///
+ /// Gets the number of save calls.
+ ///
+ public int SaveCount { get; private set; }
+
+ ///
+ /// Gets the number of delete calls.
+ ///
+ public int DeleteCount { get; private set; }
+
+ ///
+ /// Captures the supplied snapshot without persistence.
+ ///
+ /// The snapshot record.
+ /// The cancellation token.
+ /// A completed task.
+ public Task SaveAsync(
+ AIChatSessionExtractedDataRecord record,
+ CancellationToken cancellationToken = default)
+ {
+ LastSavedRecord = record;
+ SaveCount++;
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Captures a delete call without persistence.
+ ///
+ /// The session identifier.
+ /// The cancellation token.
+ /// A completed task whose result is .
+ public Task DeleteAsync(
+ string sessionId,
+ CancellationToken cancellationToken = default)
+ {
+ DeleteCount++;
+
+ return Task.FromResult(true);
+ }
+
+ ///
+ /// Returns no records.
+ ///
+ /// The profile identifier.
+ /// The inclusive start date.
+ /// The inclusive end date.
+ /// The cancellation token.
+ /// An empty record list.
+ public Task> GetAsync(
+ string profileId,
+ DateTime? startDateUtc,
+ DateTime? endDateUtc,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult>([]);
+ }
+
+ ///
+ /// Clears captured calls before benchmark measurements begin.
+ ///
+ public void Reset()
+ {
+ LastSavedRecord = null;
+ SaveCount = 0;
+ DeleteCount = 0;
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DefaultAICompletionServiceHandlerDispatchBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DefaultAICompletionServiceHandlerDispatchBenchmarks.cs
new file mode 100644
index 00000000..c4e83349
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DefaultAICompletionServiceHandlerDispatchBenchmarks.cs
@@ -0,0 +1,1039 @@
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI;
+using CrestApps.Core.AI.Clients;
+using CrestApps.Core.AI.Completions;
+using CrestApps.Core.AI.Exceptions;
+using CrestApps.Core.AI.Handlers;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.Services;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Defines completion-handler outcomes used by the dispatch benchmarks.
+///
+public enum CompletionHandlerOutcome
+{
+ ///
+ /// Every handler completes successfully.
+ ///
+ Successful,
+
+ ///
+ /// Every handler observes the context and returns the same cached faulted task.
+ ///
+ Faulting,
+}
+
+///
+/// Compares the captured completion-handler dispatch implementation with production through
+/// in-memory streaming and non-streaming clients. Operation counts normalize streaming results
+/// to nanoseconds and allocated bytes per update. This class must remain unsealed because
+/// BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+[CategoriesColumn]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+public class DefaultAICompletionServiceHandlerDispatchBenchmarks
+{
+ private CompletionDispatchScenario _oneUpdateScenario;
+ private CompletionDispatchScenario _thirtyTwoUpdateScenario;
+ private CompletionDispatchScenario _twoHundredFiftySixUpdateScenario;
+ private CompletionDispatchScenario _fourThousandNinetySixUpdateScenario;
+ private CompletionDispatchScenario _nonStreamingScenario;
+
+ ///
+ /// Gets or sets the number of completion handlers.
+ ///
+ [Params(0, 1, 4)]
+ public int HandlerCount { get; set; }
+
+ ///
+ /// Gets or sets whether handlers complete successfully or fault.
+ ///
+ [Params(CompletionHandlerOutcome.Successful, CompletionHandlerOutcome.Faulting)]
+ public CompletionHandlerOutcome Outcome { get; set; }
+
+ ///
+ /// Creates independent legacy and production services and proves exact output and observation equivalence.
+ ///
+ /// A task that completes after all equivalence checks.
+ [GlobalSetup]
+ public async Task Setup()
+ {
+ _oneUpdateScenario = new CompletionDispatchScenario(1, HandlerCount, Outcome);
+ _thirtyTwoUpdateScenario = new CompletionDispatchScenario(32, HandlerCount, Outcome);
+ _twoHundredFiftySixUpdateScenario = new CompletionDispatchScenario(256, HandlerCount, Outcome);
+ _fourThousandNinetySixUpdateScenario = new CompletionDispatchScenario(4096, HandlerCount, Outcome);
+ _nonStreamingScenario = new CompletionDispatchScenario(0, HandlerCount, Outcome);
+
+ await _oneUpdateScenario.VerifyStreamingEquivalenceAsync();
+ await _thirtyTwoUpdateScenario.VerifyStreamingEquivalenceAsync();
+ await _twoHundredFiftySixUpdateScenario.VerifyStreamingEquivalenceAsync();
+ await _fourThousandNinetySixUpdateScenario.VerifyStreamingEquivalenceAsync();
+ await _nonStreamingScenario.VerifyNonStreamingEquivalenceAsync();
+ }
+
+ ///
+ /// Releases the service providers created for this parameter combination.
+ ///
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _oneUpdateScenario.Dispose();
+ _thirtyTwoUpdateScenario.Dispose();
+ _twoHundredFiftySixUpdateScenario.Dispose();
+ _fourThousandNinetySixUpdateScenario.Dispose();
+ _nonStreamingScenario.Dispose();
+ }
+
+ ///
+ /// Streams one update with the captured implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(Baseline = true, OperationsPerInvoke = 1)]
+ [BenchmarkCategory("Streaming-1")]
+ public Task LegacyStreaming1()
+ {
+ return _oneUpdateScenario.RunLegacyStreamingAsync();
+ }
+
+ ///
+ /// Streams one update with the production implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(OperationsPerInvoke = 1)]
+ [BenchmarkCategory("Streaming-1")]
+ public Task CurrentStreaming1()
+ {
+ return _oneUpdateScenario.RunCurrentStreamingAsync();
+ }
+
+ ///
+ /// Streams 32 updates with the captured implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(Baseline = true, OperationsPerInvoke = 32)]
+ [BenchmarkCategory("Streaming-32")]
+ public Task LegacyStreaming32()
+ {
+ return _thirtyTwoUpdateScenario.RunLegacyStreamingAsync();
+ }
+
+ ///
+ /// Streams 32 updates with the production implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(OperationsPerInvoke = 32)]
+ [BenchmarkCategory("Streaming-32")]
+ public Task CurrentStreaming32()
+ {
+ return _thirtyTwoUpdateScenario.RunCurrentStreamingAsync();
+ }
+
+ ///
+ /// Streams 256 updates with the captured implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(Baseline = true, OperationsPerInvoke = 256)]
+ [BenchmarkCategory("Streaming-256")]
+ public Task LegacyStreaming256()
+ {
+ return _twoHundredFiftySixUpdateScenario.RunLegacyStreamingAsync();
+ }
+
+ ///
+ /// Streams 256 updates with the production implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(OperationsPerInvoke = 256)]
+ [BenchmarkCategory("Streaming-256")]
+ public Task CurrentStreaming256()
+ {
+ return _twoHundredFiftySixUpdateScenario.RunCurrentStreamingAsync();
+ }
+
+ ///
+ /// Streams 4,096 updates with the captured implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(Baseline = true, OperationsPerInvoke = 4096)]
+ [BenchmarkCategory("Streaming-4096")]
+ public Task LegacyStreaming4096()
+ {
+ return _fourThousandNinetySixUpdateScenario.RunLegacyStreamingAsync();
+ }
+
+ ///
+ /// Streams 4,096 updates with the production implementation.
+ ///
+ /// The emitted-update checksum.
+ [Benchmark(OperationsPerInvoke = 4096)]
+ [BenchmarkCategory("Streaming-4096")]
+ public Task CurrentStreaming4096()
+ {
+ return _fourThousandNinetySixUpdateScenario.RunCurrentStreamingAsync();
+ }
+
+ ///
+ /// Completes one non-streaming response with the captured implementation.
+ ///
+ /// The response identity checksum.
+ [Benchmark(Baseline = true)]
+ [BenchmarkCategory("NonStreaming")]
+ public Task LegacyNonStreaming()
+ {
+ return _nonStreamingScenario.RunLegacyNonStreamingAsync();
+ }
+
+ ///
+ /// Completes one non-streaming response with the production implementation.
+ ///
+ /// The response identity checksum.
+ [Benchmark]
+ [BenchmarkCategory("NonStreaming")]
+ public Task CurrentNonStreaming()
+ {
+ return _nonStreamingScenario.RunCurrentNonStreamingAsync();
+ }
+
+ ///
+ /// Owns one pair of legacy/current services and their immutable in-memory inputs.
+ ///
+ private sealed class CompletionDispatchScenario : IDisposable
+ {
+ private const string ClientName = "benchmark-client";
+ private static readonly AIDeployment _deployment = new()
+ {
+ Name = "benchmark-deployment",
+ ClientName = ClientName,
+ };
+ private static readonly ChatMessage[] _messages =
+ [
+ new(ChatRole.User, "benchmark"),
+ ];
+ private readonly InMemoryCompletionClient _client;
+ private readonly DefaultAICompletionService _currentService;
+ private readonly RecordingCompletionHandler[] _currentHandlers;
+ private readonly CountingLogger _currentLogger;
+ private readonly LegacyDefaultAICompletionService _legacyService;
+ private readonly RecordingCompletionHandler[] _legacyHandlers;
+ private readonly CountingLogger _legacyLogger;
+ private readonly ServiceProvider _serviceProvider;
+ private readonly ChatResponseUpdate[] _updates;
+
+ ///
+ /// Initializes a new dispatch scenario.
+ ///
+ /// The number of streamed updates.
+ /// The number of handlers.
+ /// The handler outcome.
+ public CompletionDispatchScenario(
+ int updateCount,
+ int handlerCount,
+ CompletionHandlerOutcome outcome)
+ {
+ _updates = CreateUpdates(updateCount);
+ _client = new InMemoryCompletionClient(_updates);
+ var services = new ServiceCollection();
+ services.AddSingleton(_client);
+ services.AddCoreAICompletionClient(ClientName);
+ _serviceProvider = services.BuildServiceProvider();
+ var options = _serviceProvider.GetRequiredService>();
+ _legacyHandlers = CreateHandlers(handlerCount, outcome);
+ _currentHandlers = CreateHandlers(handlerCount, outcome);
+ _legacyLogger = new CountingLogger();
+ _currentLogger = new CountingLogger();
+ _legacyService = new LegacyDefaultAICompletionService(
+ _serviceProvider,
+ _legacyHandlers,
+ options,
+ _legacyLogger);
+ _currentService = new DefaultAICompletionService(
+ _serviceProvider,
+ _currentHandlers,
+ options,
+ _currentLogger);
+ }
+
+ ///
+ /// Proves streamed output identity, handler observation order and identity, cancellation, and logging equivalence.
+ ///
+ /// A task that completes after equivalence verification.
+ public async Task VerifyStreamingEquivalenceAsync()
+ {
+ SetCapture(_legacyHandlers, true);
+ SetCapture(_currentHandlers, true);
+
+ var legacyUpdates = await CollectAsync(_legacyService.CompleteStreamingAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext()));
+ var currentUpdates = await CollectAsync(_currentService.CompleteStreamingAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext()));
+
+ EnsureEmittedUpdatesEquivalent(_updates, legacyUpdates, currentUpdates);
+ EnsureStreamingObservationsEquivalent(
+ _updates,
+ _legacyHandlers,
+ _currentHandlers);
+ EnsureLogCountsEquivalent(
+ _legacyLogger,
+ _currentLogger,
+ _updates.Length,
+ _legacyHandlers);
+ Reset();
+ }
+
+ ///
+ /// Proves non-streaming response identity, handler observation order and identity, cancellation, and logging equivalence.
+ ///
+ /// A task that completes after equivalence verification.
+ public async Task VerifyNonStreamingEquivalenceAsync()
+ {
+ SetCapture(_legacyHandlers, true);
+ SetCapture(_currentHandlers, true);
+
+ var legacyResponse = await _legacyService.CompleteAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext());
+ var currentResponse = await _currentService.CompleteAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext());
+
+ if (!ReferenceEquals(_client.Response, legacyResponse) ||
+ !ReferenceEquals(_client.Response, currentResponse))
+ {
+ throw new InvalidOperationException("Completion implementations returned different response references.");
+ }
+
+ EnsureNonStreamingObservationsEquivalent(
+ _client.Response,
+ _legacyHandlers,
+ _currentHandlers);
+ EnsureLogCountsEquivalent(
+ _legacyLogger,
+ _currentLogger,
+ operationCount: 1,
+ _legacyHandlers);
+ Reset();
+ }
+
+ ///
+ /// Runs the captured streaming implementation.
+ ///
+ /// The emitted-update checksum.
+ public Task RunLegacyStreamingAsync()
+ {
+ return ConsumeAsync(_legacyService.CompleteStreamingAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext()));
+ }
+
+ ///
+ /// Runs the production streaming implementation.
+ ///
+ /// The emitted-update checksum.
+ public Task RunCurrentStreamingAsync()
+ {
+ return ConsumeAsync(_currentService.CompleteStreamingAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext()));
+ }
+
+ ///
+ /// Runs the captured non-streaming implementation.
+ ///
+ /// The response identity checksum.
+ public async Task RunLegacyNonStreamingAsync()
+ {
+ var response = await _legacyService.CompleteAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext());
+
+ return RuntimeHelpers.GetHashCode(response);
+ }
+
+ ///
+ /// Runs the production non-streaming implementation.
+ ///
+ /// The response identity checksum.
+ public async Task RunCurrentNonStreamingAsync()
+ {
+ var response = await _currentService.CompleteAsync(
+ _deployment,
+ _messages,
+ new AICompletionContext());
+
+ return RuntimeHelpers.GetHashCode(response);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _serviceProvider.Dispose();
+ }
+
+ ///
+ /// Creates stable update instances.
+ ///
+ /// The update count.
+ /// The updates.
+ private static ChatResponseUpdate[] CreateUpdates(int count)
+ {
+ var updates = new ChatResponseUpdate[count];
+
+ for (var index = 0; index < updates.Length; index++)
+ {
+ updates[index] = new ChatResponseUpdate(ChatRole.Assistant, $"update-{index}");
+ }
+
+ return updates;
+ }
+
+ ///
+ /// Creates handlers with independent observation buffers.
+ ///
+ /// The handler count.
+ /// The handler outcome.
+ /// The handlers.
+ private static RecordingCompletionHandler[] CreateHandlers(
+ int handlerCount,
+ CompletionHandlerOutcome outcome)
+ {
+ var handlers = new RecordingCompletionHandler[handlerCount];
+
+ for (var index = 0; index < handlers.Length; index++)
+ {
+ handlers[index] = new RecordingCompletionHandler(index, outcome);
+ }
+
+ return handlers;
+ }
+
+ ///
+ /// Enables or disables detailed setup-only observations.
+ ///
+ /// The handlers.
+ /// Whether to capture.
+ private static void SetCapture(
+ RecordingCompletionHandler[] handlers,
+ bool capture)
+ {
+ foreach (var handler in handlers)
+ {
+ handler.Capture = capture;
+ }
+ }
+
+ ///
+ /// Collects emitted updates for setup-only equivalence verification.
+ ///
+ /// The update stream.
+ /// The emitted updates.
+ private static async Task> CollectAsync(
+ IAsyncEnumerable updates)
+ {
+ var result = new List();
+
+ await foreach (var update in updates)
+ {
+ result.Add(update);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Consumes a stream without allocating a result collection.
+ ///
+ /// The update stream.
+ /// The emitted-update checksum.
+ private static async Task ConsumeAsync(
+ IAsyncEnumerable updates)
+ {
+ var checksum = 17;
+
+ await foreach (var update in updates)
+ {
+ checksum = unchecked((checksum * 31) + RuntimeHelpers.GetHashCode(update));
+ }
+
+ return checksum;
+ }
+
+ ///
+ /// Verifies both implementations emit the same source references in the same order.
+ ///
+ /// The source updates.
+ /// The captured implementation updates.
+ /// The production implementation updates.
+ private static void EnsureEmittedUpdatesEquivalent(
+ ChatResponseUpdate[] expected,
+ List legacy,
+ List current)
+ {
+ if (legacy.Count != expected.Length ||
+ current.Count != expected.Length)
+ {
+ throw new InvalidOperationException("Completion implementations emitted different update counts.");
+ }
+
+ for (var index = 0; index < expected.Length; index++)
+ {
+ if (!ReferenceEquals(expected[index], legacy[index]) ||
+ !ReferenceEquals(expected[index], current[index]))
+ {
+ throw new InvalidOperationException(
+ $"Completion implementations emitted different update references at index {index}.");
+ }
+ }
+ }
+
+ ///
+ /// Verifies per-update handler order, shared context identity, update identity, and default handler tokens.
+ ///
+ /// The source updates.
+ /// The captured implementation handlers.
+ /// The production implementation handlers.
+ private static void EnsureStreamingObservationsEquivalent(
+ ChatResponseUpdate[] updates,
+ RecordingCompletionHandler[] legacyHandlers,
+ RecordingCompletionHandler[] currentHandlers)
+ {
+ var expectedObservationCount = updates.Length * legacyHandlers.Length;
+ var legacy = FlattenObservations(legacyHandlers);
+ var current = FlattenObservations(currentHandlers);
+
+ if (legacy.Count != expectedObservationCount ||
+ current.Count != expectedObservationCount)
+ {
+ throw new InvalidOperationException("Completion handlers observed different update counts.");
+ }
+
+ for (var updateIndex = 0; updateIndex < updates.Length; updateIndex++)
+ {
+ object legacyContext = null;
+ object currentContext = null;
+
+ for (var handlerIndex = 0; handlerIndex < legacyHandlers.Length; handlerIndex++)
+ {
+ var observationIndex = (updateIndex * legacyHandlers.Length) + handlerIndex;
+ var legacyObservation = legacy[observationIndex];
+ var currentObservation = current[observationIndex];
+
+ if (legacyObservation.HandlerIndex != handlerIndex ||
+ currentObservation.HandlerIndex != handlerIndex ||
+ !ReferenceEquals(legacyObservation.Payload, updates[updateIndex]) ||
+ !ReferenceEquals(currentObservation.Payload, updates[updateIndex]) ||
+ legacyObservation.CancellationToken.CanBeCanceled ||
+ currentObservation.CancellationToken.CanBeCanceled)
+ {
+ throw new InvalidOperationException(
+ $"Completion handlers observed different streamed values at index {observationIndex}.");
+ }
+
+ legacyContext ??= legacyObservation.Context;
+ currentContext ??= currentObservation.Context;
+
+ if (!ReferenceEquals(legacyContext, legacyObservation.Context) ||
+ !ReferenceEquals(currentContext, currentObservation.Context))
+ {
+ throw new InvalidOperationException(
+ $"Completion handlers did not share one context for update {updateIndex}.");
+ }
+ }
+
+ if (updateIndex > 0 &&
+ legacyHandlers.Length > 0)
+ {
+ var priorObservationIndex = ((updateIndex - 1) * legacyHandlers.Length);
+
+ if (ReferenceEquals(legacy[priorObservationIndex].Context, legacyContext) ||
+ ReferenceEquals(current[priorObservationIndex].Context, currentContext))
+ {
+ throw new InvalidOperationException("Completion handlers reused a context across updates.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Verifies handler order, shared context identity, response identity, and default handler tokens.
+ ///
+ /// The source response.
+ /// The captured implementation handlers.
+ /// The production implementation handlers.
+ private static void EnsureNonStreamingObservationsEquivalent(
+ ChatResponse response,
+ RecordingCompletionHandler[] legacyHandlers,
+ RecordingCompletionHandler[] currentHandlers)
+ {
+ var legacy = FlattenObservations(legacyHandlers);
+ var current = FlattenObservations(currentHandlers);
+
+ if (legacy.Count != legacyHandlers.Length ||
+ current.Count != currentHandlers.Length)
+ {
+ throw new InvalidOperationException("Completion handlers observed different response counts.");
+ }
+
+ object legacyContext = null;
+ object currentContext = null;
+
+ for (var handlerIndex = 0; handlerIndex < legacyHandlers.Length; handlerIndex++)
+ {
+ var legacyObservation = legacy[handlerIndex];
+ var currentObservation = current[handlerIndex];
+
+ if (legacyObservation.HandlerIndex != handlerIndex ||
+ currentObservation.HandlerIndex != handlerIndex ||
+ !ReferenceEquals(legacyObservation.Payload, response) ||
+ !ReferenceEquals(currentObservation.Payload, response) ||
+ legacyObservation.CancellationToken.CanBeCanceled ||
+ currentObservation.CancellationToken.CanBeCanceled)
+ {
+ throw new InvalidOperationException(
+ $"Completion handlers observed different response values at index {handlerIndex}.");
+ }
+
+ legacyContext ??= legacyObservation.Context;
+ currentContext ??= currentObservation.Context;
+
+ if (!ReferenceEquals(legacyContext, legacyObservation.Context) ||
+ !ReferenceEquals(currentContext, currentObservation.Context))
+ {
+ throw new InvalidOperationException("Completion handlers did not share one response context.");
+ }
+ }
+ }
+
+ ///
+ /// Flattens observations into invocation order.
+ ///
+ /// The handlers.
+ /// The ordered observations.
+ private static List FlattenObservations(
+ RecordingCompletionHandler[] handlers)
+ {
+ var count = handlers.Sum(static handler => handler.Observations.Count);
+ var result = new List(count);
+
+ if (handlers.Length == 0)
+ {
+ return result;
+ }
+
+ var operationCount = handlers[0].Observations.Count;
+
+ for (var operationIndex = 0; operationIndex < operationCount; operationIndex++)
+ {
+ for (var handlerIndex = 0; handlerIndex < handlers.Length; handlerIndex++)
+ {
+ result.Add(handlers[handlerIndex].Observations[operationIndex]);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Verifies fault logging counts are equivalent and match the configured outcome.
+ ///
+ /// The captured implementation logger.
+ /// The production implementation logger.
+ /// The update or response count.
+ /// The configured handlers.
+ private static void EnsureLogCountsEquivalent(
+ CountingLogger legacyLogger,
+ CountingLogger currentLogger,
+ int operationCount,
+ RecordingCompletionHandler[] handlers)
+ {
+ var expectedLogCount = handlers.Length > 0 &&
+ handlers[0].Outcome == CompletionHandlerOutcome.Faulting
+ ? operationCount * handlers.Length
+ : 0;
+
+ if (legacyLogger.LogCount != expectedLogCount ||
+ currentLogger.LogCount != expectedLogCount)
+ {
+ throw new InvalidOperationException(
+ $"Completion implementations logged {legacyLogger.LogCount} and {currentLogger.LogCount} errors; expected {expectedLogCount}.");
+ }
+ }
+
+ ///
+ /// Disables setup-only capture and clears counters before measurement.
+ ///
+ private void Reset()
+ {
+ foreach (var handler in _legacyHandlers)
+ {
+ handler.Reset();
+ }
+
+ foreach (var handler in _currentHandlers)
+ {
+ handler.Reset();
+ }
+
+ _legacyLogger.Reset();
+ _currentLogger.Reset();
+ }
+ }
+
+ ///
+ /// Captures the production completion service before dispatch optimization.
+ ///
+ private sealed class LegacyDefaultAICompletionService
+ {
+ private readonly AIOptions _aiOptions;
+ private readonly IEnumerable _completionHandlers;
+ private readonly ILogger _logger;
+ private readonly IServiceProvider _serviceProvider;
+
+ ///
+ /// Initializes a new captured completion service.
+ ///
+ /// The service provider.
+ /// The completion handlers.
+ /// The AI options.
+ /// The logger.
+ public LegacyDefaultAICompletionService(
+ IServiceProvider serviceProvider,
+ IEnumerable completionHandlers,
+ IOptions aiOptions,
+ ILogger logger)
+ {
+ _serviceProvider = serviceProvider;
+ _completionHandlers = completionHandlers;
+ _aiOptions = aiOptions.Value;
+ _logger = logger;
+ }
+
+ ///
+ /// Completes a non-streaming response with the captured handler dispatch.
+ ///
+ /// The deployment.
+ /// The messages.
+ /// The completion context.
+ /// The cancellation token.
+ /// The completion response.
+ public async Task CompleteAsync(
+ AIDeployment deployment,
+ IEnumerable messages,
+ AICompletionContext context,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(deployment);
+ ArgumentNullException.ThrowIfNull(messages);
+ ArgumentNullException.ThrowIfNull(context);
+
+ var client = ResolveClient(deployment);
+
+ var response = await client.CompleteAsync(messages, context, cancellationToken)
+ ?? throw new InvalidOperationException("Unable to generate a response. Ensure that the connection, and the deployment names are correct.");
+
+ var updateContext = new ReceivedMessageContext(response);
+
+ await InvokeHandlersAsync(handler => handler.ReceivedMessageAsync(updateContext));
+
+ return response;
+ }
+
+ ///
+ /// Streams responses with the captured handler dispatch.
+ ///
+ /// The deployment.
+ /// The messages.
+ /// The completion context.
+ /// The cancellation token.
+ /// The completion update stream.
+ public async IAsyncEnumerable CompleteStreamingAsync(
+ AIDeployment deployment,
+ IEnumerable messages,
+ AICompletionContext context,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(deployment);
+ ArgumentNullException.ThrowIfNull(messages);
+ ArgumentNullException.ThrowIfNull(context);
+
+ var client = ResolveClient(deployment);
+
+ await foreach (var chunk in client.CompleteStreamingAsync(messages, context, cancellationToken))
+ {
+ var updateContext = new ReceivedUpdateContext(chunk);
+
+ await InvokeHandlersAsync(handler => handler.ReceivedUpdateAsync(updateContext));
+
+ yield return chunk;
+ }
+ }
+
+ ///
+ /// Invokes handlers with the captured delegate-based dispatch.
+ ///
+ /// The handler delegate.
+ /// A task representing handler dispatch.
+ private async Task InvokeHandlersAsync(Func invoke)
+ {
+ foreach (var handler in _completionHandlers)
+ {
+ try
+ {
+ await invoke(handler);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error invoking completion handler '{HandlerType}'.", handler.GetType().Name);
+ }
+ }
+ }
+
+ ///
+ /// Resolves the configured in-memory client.
+ ///
+ /// The deployment.
+ /// The completion client.
+ private IAICompletionClient ResolveClient(AIDeployment deployment)
+ {
+ var clientName = deployment.ClientName
+ ?? throw new AIDeploymentConfigurationException($"The deployment '{deployment.Name}' does not have a client name assigned.");
+
+ if (!_aiOptions.Clients.TryGetValue(clientName, out var clientType))
+ {
+ throw new UnregisteredCompletionClientException(clientName);
+ }
+
+ return _serviceProvider.GetService(clientType) as IAICompletionClient
+ ?? throw new InvalidOperationException($"No completion client registered for '{clientName}'.");
+ }
+ }
+
+ ///
+ /// Emits pre-created responses and updates without network or provider work.
+ ///
+ private sealed class InMemoryCompletionClient : IAICompletionClient
+ {
+ private readonly ChatResponseUpdate[] _updates;
+
+ ///
+ /// Initializes a new in-memory completion client.
+ ///
+ /// The updates to emit.
+ public InMemoryCompletionClient(ChatResponseUpdate[] updates)
+ {
+ _updates = updates;
+ Response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "done"));
+ }
+
+ ///
+ public string ClientName => "benchmark-client";
+
+ ///
+ /// Gets the stable non-streaming response.
+ ///
+ public ChatResponse Response { get; }
+
+ ///
+ public Task CompleteAsync(
+ IEnumerable messages,
+ AICompletionContext context,
+ CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(Response);
+ }
+
+ ///
+ public async IAsyncEnumerable CompleteStreamingAsync(
+ IEnumerable messages,
+ AICompletionContext context,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ for (var index = 0; index < _updates.Length; index++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ yield return _updates[index];
+ }
+
+ await Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// Observes handler inputs and optionally returns a cached faulted task.
+ ///
+ private sealed class RecordingCompletionHandler : AICompletionHandlerBase
+ {
+ private readonly Task _result;
+
+ ///
+ /// Initializes a new recording handler.
+ ///
+ /// The handler index.
+ /// The handler outcome.
+ public RecordingCompletionHandler(
+ int handlerIndex,
+ CompletionHandlerOutcome outcome)
+ {
+ HandlerIndex = handlerIndex;
+ Outcome = outcome;
+ _result = outcome == CompletionHandlerOutcome.Faulting
+ ? Task.FromException(new InvalidOperationException("benchmark handler failure"))
+ : Task.CompletedTask;
+ }
+
+ ///
+ /// Gets or sets whether detailed setup-only observations are captured.
+ ///
+ public bool Capture { get; set; }
+
+ ///
+ /// Gets the handler index.
+ ///
+ public int HandlerIndex { get; }
+
+ ///
+ /// Gets the configured outcome.
+ ///
+ public CompletionHandlerOutcome Outcome { get; }
+
+ ///
+ /// Gets the setup-only observations.
+ ///
+ public List Observations { get; } = [];
+
+ ///
+ public override Task ReceivedMessageAsync(
+ ReceivedMessageContext context,
+ CancellationToken cancellationToken = default)
+ {
+ if (Capture)
+ {
+ Observations.Add(new HandlerObservation(
+ HandlerIndex,
+ context,
+ context.Completion,
+ cancellationToken));
+ }
+
+ return _result;
+ }
+
+ ///
+ public override Task ReceivedUpdateAsync(
+ ReceivedUpdateContext context,
+ CancellationToken cancellationToken = default)
+ {
+ if (Capture)
+ {
+ Observations.Add(new HandlerObservation(
+ HandlerIndex,
+ context,
+ context.Update,
+ cancellationToken));
+ }
+
+ return _result;
+ }
+
+ ///
+ /// Clears setup observations and disables capture.
+ ///
+ public void Reset()
+ {
+ Capture = false;
+ Observations.Clear();
+ }
+ }
+
+ ///
+ /// Represents one setup-only handler observation.
+ ///
+ /// The handler index.
+ /// The context reference.
+ /// The update or response reference.
+ /// The handler cancellation token.
+ private sealed record HandlerObservation(
+ int HandlerIndex,
+ object Context,
+ object Payload,
+ CancellationToken CancellationToken);
+
+ ///
+ /// Counts logging calls without retaining state or formatting messages.
+ ///
+ private sealed class CountingLogger : ILogger
+ {
+ ///
+ /// Gets the number of logging calls.
+ ///
+ public int LogCount { get; private set; }
+
+ ///
+ public IDisposable BeginScope(TState state)
+ where TState : notnull
+ {
+ return NoOpScope.Instance;
+ }
+
+ ///
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return false;
+ }
+
+ ///
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
+ {
+ LogCount++;
+ }
+
+ ///
+ /// Resets the logging count.
+ ///
+ public void Reset()
+ {
+ LogCount = 0;
+ }
+ }
+
+ ///
+ /// Provides a reusable no-op logging scope.
+ ///
+ private sealed class NoOpScope : IDisposable
+ {
+ ///
+ /// Gets the singleton scope.
+ ///
+ public static NoOpScope Instance { get; } = new();
+
+ ///
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/benchmarks/CrestApps.Core.Benchmarks/DefaultMcpCapabilityResolverBenchmarks.cs b/benchmarks/CrestApps.Core.Benchmarks/DefaultMcpCapabilityResolverBenchmarks.cs
new file mode 100644
index 00000000..137f109d
--- /dev/null
+++ b/benchmarks/CrestApps.Core.Benchmarks/DefaultMcpCapabilityResolverBenchmarks.cs
@@ -0,0 +1,1120 @@
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using CrestApps.Core.AI;
+using CrestApps.Core.AI.Clients;
+using CrestApps.Core.AI.Deployments;
+using CrestApps.Core.AI.Mcp;
+using CrestApps.Core.AI.Mcp.Models;
+using CrestApps.Core.AI.Mcp.Services;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.Speech;
+using CrestApps.Core.Models;
+using CrestApps.Core.Services;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace CrestApps.Core.Benchmarks;
+
+///
+/// Measures MCP capability construction, keyword matching, duplicate merging, stable ties,
+/// and bounded ranking with a legacy implementation captured before optimization.
+/// This class must remain unsealed because BenchmarkDotNet generates a derived benchmark type.
+///
+[MemoryDiagnoser]
+[SimpleJob(warmupCount: 5, iterationCount: 12)]
+public class DefaultMcpCapabilityResolverBenchmarks
+{
+ private const string _prompt = "search customer documents and summarize account activity";
+
+ private IMcpCapabilityResolver _currentResolver;
+ private LegacyDefaultMcpCapabilityResolver _legacyResolver;
+ private string[] _connectionIds;
+
+ ///
+ /// Gets or sets the total number of raw capabilities exposed by the benchmark servers.
+ ///
+ [Params(100, 1000, 10000)]
+ public int CandidateCount { get; set; }
+
+ ///
+ /// Gets or sets the small maximum result count used after merging and ranking.
+ ///
+ [Params(3, 5)]
+ public int TopK { get; set; }
+
+ ///
+ /// Gets or sets whether the benchmark uses the production Lucene tokenizer.
+ /// The false case isolates capability construction, duplicate merging, and ranking.
+ ///
+ [Params(false, true)]
+ public bool UseLuceneTokenizer { get; set; }
+
+ ///
+ /// Creates equivalent legacy and current resolvers over immutable in-memory servers.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var fixture = CreateFixture(CandidateCount);
+ var options = new McpCapabilityResolverOptions
+ {
+ IncludeAllThreshold = 0,
+ KeywordMatchThreshold = 0.2f,
+ SimilarityThreshold = 0.3f,
+ TopK = TopK,
+ };
+ ITextTokenizer tokenizer = UseLuceneTokenizer
+ ? new LuceneTextTokenizer()
+ : new CategorizedTextTokenizer();
+
+ _connectionIds = fixture.Connections
+ .Select(connection => connection.ItemId)
+ .ToArray();
+ _legacyResolver = new LegacyDefaultMcpCapabilityResolver(
+ fixture.Store,
+ fixture.MetadataProvider,
+ null,
+ null,
+ null,
+ tokenizer,
+ Options.Create(options),
+ NullLogger.Instance);
+ _currentResolver = CreateCurrentResolver(
+ fixture.Store,
+ fixture.MetadataProvider,
+ tokenizer,
+ options);
+
+ var legacy = _legacyResolver
+ .ResolveAsync(_prompt, null, _connectionIds)
+ .GetAwaiter()
+ .GetResult();
+ var current = _currentResolver
+ .ResolveAsync(_prompt, null, _connectionIds)
+ .GetAwaiter()
+ .GetResult();
+
+ EnsureEquivalent(legacy, current);
+ }
+
+ ///
+ /// Resolves capabilities with the implementation captured before optimization experiments.
+ ///
+ /// The resolution result.
+ [Benchmark(Baseline = true)]
+ public Task ResolveLegacyAsync()
+ {
+ return _legacyResolver.ResolveAsync(_prompt, null, _connectionIds);
+ }
+
+ ///
+ /// Resolves capabilities with the production implementation.
+ ///
+ /// The resolution result.
+ [Benchmark]
+ public Task ResolveCurrentAsync()
+ {
+ return _currentResolver.ResolveAsync(_prompt, null, _connectionIds);
+ }
+
+ ///
+ /// Creates realistic mixed capabilities across multiple MCP servers.
+ ///
+ /// The total raw capability count.
+ /// The benchmark fixture.
+ private static BenchmarkFixture CreateFixture(int candidateCount)
+ {
+ const int serverCount = 8;
+
+ var connections = new McpConnection[serverCount];
+ var tools = CreateLists(serverCount);
+ var prompts = CreateLists(serverCount);
+ var resources = CreateLists(serverCount);
+ var resourceTemplates = CreateLists(serverCount);
+
+ for (var serverIndex = 0; serverIndex < serverCount; serverIndex++)
+ {
+ connections[serverIndex] = new McpConnection
+ {
+ ItemId = $"server-{serverIndex}",
+ DisplayText = $"Benchmark server {serverIndex}",
+ Source = "benchmark",
+ };
+ }
+
+ for (var index = 0; index < candidateCount; index++)
+ {
+ var group = index / 12;
+ var serverIndex = group % serverCount;
+
+ switch (index % 12)
+ {
+ case 0:
+ tools[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"searchCustomerDocuments{group}",
+ Description = "Search customer documents with filters, citations, permissions, and account metadata.",
+ });
+ break;
+ case 1:
+ prompts[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"summarizeAccountActivity{group}",
+ Description = "Summarize account activity, customer transactions, balances, and support history.",
+ });
+ break;
+ case 2:
+ resources[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"customerDocument{group}",
+ Description = "Customer document archive entry.",
+ Uri = $"customer://documents/account-activity/{group}",
+ });
+ break;
+ case 3:
+ resourceTemplates[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"customerDocumentTemplate{group}",
+ Description = "Loads a customer document and account activity by identifier.",
+ UriTemplate = "customer://documents/{customerId}/{documentId}",
+ });
+ break;
+ case 4:
+ tools[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"forecastWeather{group}",
+ Description = "Returns regional temperature, precipitation, and wind conditions.",
+ });
+ break;
+ case 5:
+ tools[serverIndex].Add(new McpServerCapability
+ {
+ Name = "sharedSearch",
+ Description = "Search customer documents and summarize account activity.",
+ });
+ break;
+ case 6:
+ prompts[serverIndex].Add(new McpServerCapability
+ {
+ Name = "sharedSearch",
+ Description = "Search customer documents and summarize account activity.",
+ });
+ break;
+ case 7:
+ prompts[serverIndex].Add(new McpServerCapability
+ {
+ Name = "summarizeAccount",
+ Description = "Summarize customer account activity.",
+ });
+ break;
+ case 8:
+ resources[serverIndex].Add(new McpServerCapability
+ {
+ Name = "accountActivity",
+ Description = "Customer account activity.",
+ Uri = "customer://account/activity",
+ });
+ break;
+ case 9:
+ tools[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"lookupCustomer{group}",
+ Description = null,
+ });
+ break;
+ case 10:
+ prompts[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"reviewDocuments{group}",
+ Description = " ",
+ });
+ break;
+ default:
+ resources[serverIndex].Add(new McpServerCapability
+ {
+ Name = $"teamCalendar{group}",
+ Description = "Schedules meetings and sends calendar invitations.",
+ Uri = $"calendar://team/{group}",
+ });
+ break;
+ }
+ }
+
+ var capabilities = new McpServerCapabilities[serverCount];
+
+ for (var serverIndex = 0; serverIndex < serverCount; serverIndex++)
+ {
+ capabilities[serverIndex] = new McpServerCapabilities
+ {
+ ConnectionId = connections[serverIndex].ItemId,
+ ConnectionDisplayText = connections[serverIndex].DisplayText,
+ Tools = tools[serverIndex],
+ Prompts = prompts[serverIndex],
+ Resources = resources[serverIndex],
+ ResourceTemplates = resourceTemplates[serverIndex],
+ IsHealthy = true,
+ };
+ }
+
+ var store = new BenchmarkSourceCatalog(connections);
+ var metadataProvider = new BenchmarkMetadataProvider(capabilities);
+
+ return new BenchmarkFixture(connections, store, metadataProvider);
+ }
+
+ ///
+ /// Creates the internal production resolver through its public interface.
+ ///
+ /// The connection store.
+ /// The metadata provider.
+ /// The tokenizer.
+ /// The resolver options.
+ /// The production resolver.
+ private static IMcpCapabilityResolver CreateCurrentResolver(
+ ISourceCatalog store,
+ IMcpServerMetadataCacheProvider metadataProvider,
+ ITextTokenizer tokenizer,
+ McpCapabilityResolverOptions options)
+ {
+ var resolverType = typeof(IMcpCapabilityResolver).Assembly.GetType(
+ "CrestApps.Core.AI.Mcp.Services.DefaultMcpCapabilityResolver",
+ throwOnError: true);
+ var loggerType = typeof(NullLogger<>).MakeGenericType(resolverType);
+ var logger = Activator.CreateInstance(loggerType);
+ var resolver = Activator.CreateInstance(
+ resolverType,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ binder: null,
+ args:
+ [
+ store,
+ metadataProvider,
+ null,
+ null,
+ null,
+ tokenizer,
+ Options.Create(options),
+ logger,
+ ],
+ culture: null);
+
+ return (IMcpCapabilityResolver)resolver;
+ }
+
+ ///
+ /// Creates one mutable capability list per server.
+ ///
+ /// The server count.
+ /// The lists.
+ private static List[] CreateLists(int serverCount)
+ {
+ var lists = new List[serverCount];
+
+ for (var index = 0; index < lists.Length; index++)
+ {
+ lists[index] = [];
+ }
+
+ return lists;
+ }
+
+ ///
+ /// Verifies exact result equivalence before benchmarks execute.
+ ///
+ /// The legacy result.
+ /// The current result.
+ private static void EnsureEquivalent(
+ McpCapabilityResolutionResult legacy,
+ McpCapabilityResolutionResult current)
+ {
+ var legacyCandidates = legacy.Candidates
+ .Select(candidate => (
+ candidate.ConnectionId,
+ candidate.ConnectionDisplayText,
+ candidate.CapabilityName,
+ candidate.CapabilityDescription,
+ candidate.CapabilityType,
+ candidate.SimilarityScore));
+ var currentCandidates = current.Candidates
+ .Select(candidate => (
+ candidate.ConnectionId,
+ candidate.ConnectionDisplayText,
+ candidate.CapabilityName,
+ candidate.CapabilityDescription,
+ candidate.CapabilityType,
+ candidate.SimilarityScore));
+
+ if (!legacyCandidates.SequenceEqual(currentCandidates))
+ {
+ throw new InvalidOperationException("Legacy and current MCP capability results differ.");
+ }
+ }
+
+ private sealed record BenchmarkFixture(
+ IReadOnlyList Connections,
+ BenchmarkSourceCatalog Store,
+ BenchmarkMetadataProvider MetadataProvider);
+
+ private sealed class BenchmarkSourceCatalog : ISourceCatalog
+ {
+ private readonly IReadOnlyList _connections;
+
+ ///
+ /// Initializes an immutable benchmark catalog.
+ ///
+ /// The connections.
+ public BenchmarkSourceCatalog(IReadOnlyList connections)
+ {
+ _connections = connections;
+ }
+
+ ///
+ /// Returns connections matching the requested identifiers in catalog order.
+ ///
+ /// The identifiers.
+ /// The cancellation token.
+ /// The matching connections.
+ public ValueTask> GetAsync(
+ IEnumerable ids,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var idSet = ids.ToHashSet(StringComparer.OrdinalIgnoreCase);
+ IReadOnlyCollection result = _connections
+ .Where(connection => idSet.Contains(connection.ItemId))
+ .ToArray();
+
+ return ValueTask.FromResult(result);
+ }
+
+ ///
+ /// Throws because source filtering is not used by the benchmark.
+ ///
+ /// The source.
+ /// The cancellation token.
+ public ValueTask> GetAsync(
+ string source,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because full enumeration is not used by the benchmark.
+ ///
+ /// The cancellation token.
+ public ValueTask> GetAllAsync(
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because identifier lookup is not used by the benchmark.
+ ///
+ /// The identifier.
+ /// The cancellation token.
+ public ValueTask FindByIdAsync(
+ string id,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because paging is not used by the benchmark.
+ ///
+ /// The query type.
+ /// The page.
+ /// The page size.
+ /// The query context.
+ /// The cancellation token.
+ public ValueTask> PageAsync(
+ int page,
+ int pageSize,
+ TQuery context,
+ CancellationToken cancellationToken = default)
+ where TQuery : QueryContext
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because writes are not used by the benchmark.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ public ValueTask CreateAsync(
+ McpConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because writes are not used by the benchmark.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ public ValueTask UpdateAsync(
+ McpConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Throws because writes are not used by the benchmark.
+ ///
+ /// The entry.
+ /// The cancellation token.
+ public ValueTask DeleteAsync(
+ McpConnection entry,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ private sealed class BenchmarkMetadataProvider : IMcpServerMetadataCacheProvider
+ {
+ private readonly Dictionary _capabilities;
+
+ ///
+ /// Initializes an immutable benchmark metadata provider.
+ ///
+ /// The server capabilities.
+ public BenchmarkMetadataProvider(IEnumerable capabilities)
+ {
+ _capabilities = capabilities.ToDictionary(
+ server => server.ConnectionId,
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Returns metadata for the requested connection.
+ ///
+ /// The connection.
+ /// The server capabilities.
+ public Task GetCapabilitiesAsync(McpConnection connection)
+ {
+ _capabilities.TryGetValue(connection.ItemId, out var capabilities);
+
+ return Task.FromResult(capabilities);
+ }
+
+ ///
+ /// Completes because benchmark metadata is immutable.
+ ///
+ /// The connection identifier.
+ public Task InvalidateAsync(string connectionId)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class CategorizedTextTokenizer : ITextTokenizer
+ {
+ private static readonly HashSet _promptTokens =
+ [
+ "search",
+ "customer",
+ "document",
+ "summarize",
+ "account",
+ "activity",
+ ];
+
+ private static readonly HashSet _strongTokens =
+ [
+ "search",
+ "customer",
+ "document",
+ "summarize",
+ "account",
+ "activity",
+ "citation",
+ "permission",
+ ];
+
+ private static readonly HashSet _summaryTokens =
+ [
+ "customer",
+ "summarize",
+ "account",
+ "activity",
+ "transaction",
+ "balance",
+ ];
+
+ private static readonly HashSet _resourceTokens =
+ [
+ "customer",
+ "document",
+ "account",
+ "activity",
+ "archive",
+ "identifier",
+ ];
+
+ private static readonly HashSet _weakTokens =
+ [
+ "customer",
+ "lookup",
+ "review",
+ "record",
+ ];
+
+ ///