-
Notifications
You must be signed in to change notification settings - Fork 1.3k
.NET Compaction - Introducing compaction strategies and pipeline #4533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
108 commits
Select commit
Hold shift + click to select a range
23cf75b
Checkpoint
crickman fcd60da
Checkpoint
crickman eb84062
Stable
crickman 0e8b9b2
Strategies
crickman b275d34
Merge branch 'main' into crickman/feature-compaction-deux
crickman dda15ea
Updated
crickman 7608005
Encoding
crickman 1428286
Formatting
crickman f70423b
Cleanup
crickman defb9dd
Formatting
crickman 6ce0447
Tests
crickman 7e2c5ad
Tuning
crickman 06f55c0
Update tests
crickman f42863e
Test update
crickman c513694
Remove working solution
crickman 1a8a58f
Merge branch 'main' into crickman/feature-compaction-deux
crickman 43d226f
Add sample to solution
crickman 5ef100c
Sample readyme
crickman 4d6e1ff
Experimental
crickman 2f443a1
Format
crickman 209d0e3
Formatting
crickman 84aa392
Encoding
crickman 7c88b20
Merge branch 'main' into crickman/feature-compaction-deux
crickman 9c1165f
Support IChatReducer
crickman 6ef397e
Merge branch 'main' into crickman/feature-compaction-deux
crickman 7afed95
Sample output formatting
crickman 36ba6b0
Merge branch 'main' into crickman/feature-compaction-deux
crickman 1598991
Initial plan
Copilot dc2bb4d
Replace CompactingChatClient with MessageCompactionContextProvider
Copilot 601eddb
Boundary condition
crickman cc441d2
Merge branch 'main' into crickman/feature-compaction-deux
crickman 14aae1f
Merge branch 'crickman/feature-compaction-deux' into copilot/create-m…
crickman 094b415
Fix encoding
crickman 93728c1
Fix cast
crickman aff1d06
Test coverage
crickman 04f29e6
Merge branch 'crickman/feature-compaction-deux' into copilot/create-m…
crickman 278912b
Namespace
crickman 576f750
Improvements
crickman aa47a14
Efficiency
crickman b202b7c
Cleanup
crickman 9f0cc62
Resolve merge
crickman da4886f
Detect service managed conversation
crickman dcf4b1a
Fix namespace
crickman fe09a1e
Fix merge
crickman b6070fd
Fix test expectation
crickman 06787d0
Merge branch 'main' into crickman/feature-compaction-deux
crickman cf4fe99
Merge branch 'crickman/feature-compaction-deux' into copilot/create-m…
crickman ff3d9e5
Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistor…
crickman 1fffafe
Address PR comments (x1)
crickman e157b5f
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman d6d2331
Update comment
crickman ee936d3
Update comments
crickman f42ebb0
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman b6cbf62
Clean-up
crickman 6aeb295
Format output
crickman f88fed0
Sync sample comment
crickman 9edd440
Fix condition
crickman f105ae0
Adjust data-flow
crickman f40710f
Address comments (x2)
crickman 39bd2a5
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 5c406b8
Direct compaction
crickman b0138dc
Fix summarization content
crickman 8179e2e
Argument check / fix count calculation
crickman 4629cc3
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 011995b
Minor follow-up
crickman c68a7d3
Diagnostics
crickman c010d70
Minor updates
crickman 6df1e23
Fix state test
crickman 08354f4
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman 133a631
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman 2c37cfa
Fix sliding window perf
crickman 7640783
Stable state keys
crickman 9a46d18
Increase size computation
crickman 60c4b7a
Formatting
crickman 1287881
Add README.md for Agent_Step18_CompactionPipeline sample (#4574)
Copilot 19643d3
Sample comments
crickman cc8cfc7
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman e79ca62
Updated
crickman 302b4f4
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 182cefb
Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs
crickman 4b64b28
Update dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/Compacti…
crickman 2d8f53f
Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs
crickman 10f3538
Address copilot comments
crickman 9e78a1e
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman 1ae8d25
Fix namespace
crickman 2c7e1c8
Merge branch 'main' into copilot/create-message-compaction-provider
crickman dfff5fc
Comments / convensions
crickman 48fbe27
Prefix `MessageGroup` and `MessageIndex`
crickman d8d0d1c
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 1c5820e
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 70ae245
Fix sliding window
crickman d1cadea
Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompact…
crickman efe5f67
Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistor…
crickman 3821ae7
Python alignment
crickman 235876e
Resolve merge
crickman 6d663f2
Fix merge
crickman ee25c42
Merge branch 'main' into copilot/create-message-compaction-provider
crickman 6fbe1ec
Fix equality, readme, and sample
crickman 3cdb4c0
Readme update and ToolResult fix
crickman 45d689b
Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompact…
crickman e79cfd1
Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipelin…
crickman 5023ffc
Simplify readme
crickman 932385b
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman e4beb84
Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipelin…
crickman 07c9af7
Remove example
crickman ffc1603
Merge branch 'copilot/create-message-compaction-provider' of https://…
crickman f75f796
Remove unused
crickman 339117e
Merge branch 'main' into copilot/create-message-compaction-provider
crickman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
...s/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>net10.0</TargetFrameworks> | ||
|
|
||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Azure.AI.OpenAI" /> | ||
| <PackageReference Include="Azure.Identity" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI.OpenAI" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
120 changes: 120 additions & 0 deletions
120
dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates how to use a CompactionProvider with a compaction pipeline | ||
| // as an AIContextProvider for an agent's in-run context management. The pipeline chains multiple | ||
| // compaction strategies from gentle to aggressive: | ||
| // 1. ToolResultCompactionStrategy - Collapses old tool-call groups into concise summaries | ||
| // 2. SummarizationCompactionStrategy - LLM-compresses older conversation spans | ||
| // 3. SlidingWindowCompactionStrategy - Keeps only the most recent N user turns | ||
| // 4. TruncationCompactionStrategy - Emergency token-budget backstop | ||
|
|
||
| using System.ComponentModel; | ||
| using Azure.AI.OpenAI; | ||
| using Azure.Identity; | ||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Agents.AI.Compaction; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); | ||
| var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; | ||
|
|
||
| // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. | ||
| // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid | ||
| // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. | ||
| AzureOpenAIClient openAIClient = new(new Uri(endpoint), new DefaultAzureCredential()); | ||
|
|
||
| // Create a chat client for the agent and a separate one for the summarization strategy. | ||
| // Using the same model for simplicity; in production, use a smaller/cheaper model for summarization. | ||
| IChatClient agentChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient(); | ||
| IChatClient summarizerChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient(); | ||
|
|
||
| // Define a tool the agent can use, so we can see tool-result compaction in action. | ||
| [Description("Look up the current price of a product by name.")] | ||
| static string LookupPrice([Description("The product name to look up.")] string productName) => | ||
| productName.ToUpperInvariant() switch | ||
| { | ||
| "LAPTOP" => "The laptop costs $999.99.", | ||
| "KEYBOARD" => "The keyboard costs $79.99.", | ||
| "MOUSE" => "The mouse costs $29.99.", | ||
| _ => $"Sorry, I don't have pricing for '{productName}'." | ||
| }; | ||
|
|
||
| // Configure the compaction pipeline with one of each strategy, ordered least to most aggressive. | ||
| PipelineCompactionStrategy compactionPipeline = | ||
| new(// 1. Gentle: collapse old tool-call groups into short summaries | ||
| new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(7)), | ||
|
|
||
| // 2. Moderate: use an LLM to summarize older conversation spans into a concise message | ||
| new SummarizationCompactionStrategy(summarizerChatClient, CompactionTriggers.TokensExceed(0x500)), | ||
|
|
||
| // 3. Aggressive: keep only the last N user turns and their responses | ||
| new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(4)), | ||
|
|
||
| // 4. Emergency: drop oldest groups until under the token budget | ||
| new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(0x8000))); | ||
|
|
||
| // Create the agent with a CompactionProvider that uses the compaction pipeline. | ||
| AIAgent agent = | ||
| agentChatClient | ||
| .AsBuilder() | ||
| // Note: Adding the CompactionProvider at the builder level means it will be applied to all agents | ||
| // built from this builder and will manage context for both agent messages and tool calls. | ||
| .UseAIContextProviders(new CompactionProvider(compactionPipeline)) | ||
| .BuildAIAgent( | ||
| new ChatClientAgentOptions | ||
| { | ||
| Name = "ShoppingAssistant", | ||
| ChatOptions = new() | ||
| { | ||
| Instructions = | ||
| """ | ||
| You are a helpful, but long winded, shopping assistant. | ||
| Help the user look up prices and compare products. | ||
| When responding, Be sure to be extra descriptive and use as | ||
| many words as possible without sounding ridiculous. | ||
| """, | ||
| Tools = [AIFunctionFactory.Create(LookupPrice)] | ||
| }, | ||
| // Note: AIContextProviders may be specified here instead of ChatClientBuilder.UseAIContextProviders. | ||
| // Specifying compaction at the agent level skips compaction in the function calling loop. | ||
| //AIContextProviders = [new CompactionProvider(compactionPipeline)] | ||
| }); | ||
|
|
||
| AgentSession session = await agent.CreateSessionAsync(); | ||
|
|
||
| // Helper to print chat history size | ||
| void PrintChatHistory() | ||
| { | ||
| if (session.TryGetInMemoryChatHistory(out var history)) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Cyan; | ||
| Console.WriteLine($"\n[Messages: #{history.Count}]\n"); | ||
| Console.ResetColor(); | ||
| } | ||
| } | ||
|
|
||
| // Run a multi-turn conversation with tool calls to exercise the pipeline. | ||
| string[] prompts = | ||
| [ | ||
| "What's the price of a laptop?", | ||
| "How about a keyboard?", | ||
| "And a mouse?", | ||
| "Which product is the cheapest?", | ||
| "Can you compare the laptop and the keyboard for me?", | ||
| "What was the first product I asked about?", | ||
| "Thank you!", | ||
| ]; | ||
|
|
||
| foreach (string prompt in prompts) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Cyan; | ||
| Console.Write("\n[User] "); | ||
| Console.ResetColor(); | ||
| Console.WriteLine(prompt); | ||
| Console.ForegroundColor = ConsoleColor.Cyan; | ||
| Console.Write("\n[Agent] "); | ||
| Console.ResetColor(); | ||
| Console.WriteLine(await agent.RunAsync(prompt, session)); | ||
|
|
||
| PrintChatHistory(); | ||
| } | ||
132 changes: 132 additions & 0 deletions
132
dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # Compaction Pipeline | ||
|
|
||
| This sample demonstrates how to use a `CompactionProvider` with a `PipelineCompactionStrategy` to manage long conversation histories in a token-efficient way. The pipeline chains four compaction strategies, ordered from gentle to aggressive, so that the least disruptive strategy runs first and more aggressive strategies only activate when necessary. | ||
|
|
||
| ## What This Sample Shows | ||
|
|
||
| - **`CompactionProvider`** — an `AIContextProvider` that applies a compaction strategy before each agent invocation, keeping only the most relevant messages within the model's context window | ||
| - **`PipelineCompactionStrategy`** — chains multiple compaction strategies into an ordered pipeline; each strategy evaluates its own trigger independently and operates on the output of the previous one | ||
| - **`ToolResultCompactionStrategy`** — collapses older tool-call groups into concise inline summaries, activated by a message-count trigger | ||
crickman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - **`SummarizationCompactionStrategy`** — uses an LLM to compress older conversation spans into a single summary message, activated by a token-count trigger | ||
| - **`SlidingWindowCompactionStrategy`** — retains only the most recent N user turns and their responses, activated by a turn-count trigger | ||
| - **`TruncationCompactionStrategy`** — emergency backstop that drops the oldest groups until the conversation fits within a hard token budget | ||
| - **`CompactionTriggers`** — factory methods (`MessagesExceed`, `TokensExceed`, `TurnsExceed`, `GroupsExceed`, `HasToolCalls`, `All`, `Any`) that control when each strategy activates | ||
|
|
||
| ## Concepts | ||
|
|
||
| ### Message groups | ||
|
|
||
| The compaction engine organizes messages into atomic *groups* that are treated as indivisible units during compaction. A group is either: | ||
|
|
||
| | Group kind | Contents | | ||
| |---|---| | ||
| | `System` | System prompt message(s) | | ||
| | `User` | A single user message | | ||
| | `ToolCall` | One assistant message with tool calls + the matching tool result messages | | ||
| | `AssistantText` | A single assistant text-only message | | ||
| | `Summary` | One or more messages summarizing earlier conversation spans, produced by compaction strategies | | ||
|
|
||
| `Summary` groups (`CompactionGroupKind.Summary`) are created by compaction strategies (for example, `SummarizationCompactionStrategy`) and do not originate directly from user or assistant messages. | ||
| Strategies exclude entire groups rather than individual messages, preserving the tool-call/result pairing required by most model APIs. | ||
|
|
||
| ### Compaction triggers | ||
|
|
||
| A `CompactionTrigger` is a predicate evaluated against the current `MessageIndex`. When the trigger fires, the strategy performs compaction; when it does not fire, the strategy is skipped. Available triggers are: | ||
|
|
||
| | Trigger | Activates when… | | ||
| |---|---| | ||
| | `CompactionTriggers.Always` | Always (unconditional) | | ||
| | `CompactionTriggers.Never` | Never (disabled) | | ||
| | `CompactionTriggers.MessagesExceed(n)` | Included message count > n | | ||
| | `CompactionTriggers.TokensExceed(n)` | Included token count > n | | ||
| | `CompactionTriggers.TurnsExceed(n)` | Included user-turn count > n | | ||
| | `CompactionTriggers.GroupsExceed(n)` | Included group count > n | | ||
| | `CompactionTriggers.HasToolCalls()` | At least one included tool-call group exists | | ||
| | `CompactionTriggers.All(...)` | All supplied triggers fire (logical AND) | | ||
| | `CompactionTriggers.Any(...)` | Any supplied trigger fires (logical OR) | | ||
|
|
||
| ### Pipeline ordering | ||
|
|
||
| Order strategies from **least aggressive** to **most aggressive**. The pipeline runs every strategy whose trigger is met. Earlier strategies reduce the conversation gently so that later, more destructive strategies may not need to activate at all. | ||
|
|
||
| ``` | ||
| 1. ToolResultCompactionStrategy – gentle: replaces verbose tool results with a short label | ||
| 2. SummarizationCompactionStrategy – moderate: LLM-summarizes older turns | ||
| 3. SlidingWindowCompactionStrategy – aggressive: drops turns beyond the window | ||
| 4. TruncationCompactionStrategy – emergency: hard token-budget enforcement | ||
| ``` | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - .NET 10 SDK or later | ||
| - Azure OpenAI service endpoint and model deployment | ||
| - Azure CLI installed and authenticated | ||
|
|
||
| **Note**: This sample uses `DefaultAzureCredential`. Sign in with `az login` before running. For production, prefer a specific credential such as `ManagedIdentityCredential`. For more information, see the [Azure CLI authentication documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| ```powershell | ||
| $env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Required | ||
| $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini | ||
| ``` | ||
|
|
||
| ## Running the Sample | ||
|
|
||
| ```powershell | ||
| cd dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline | ||
| dotnet run | ||
| ``` | ||
|
|
||
| ## Expected Behavior | ||
|
|
||
| The sample runs a seven-turn shopping-assistant conversation with tool calls. After each turn it prints the full message count so you can observe the pipeline compaction doesn't alter the source conversation. | ||
|
|
||
| Each of the four compaction strategies has a deliberately low threshold so that it activates during the short demonstration conversation. In a production scenario you would raise the thresholds to match your model's context window and cost requirements. | ||
|
|
||
| ## Customizing the Pipeline | ||
|
|
||
| ### Using a single strategy | ||
|
|
||
| If you only need one compaction strategy, pass it directly to `CompactionProvider` without wrapping it in a pipeline: | ||
|
|
||
| ```csharp | ||
| CompactionProvider provider = | ||
| new(new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(20))); | ||
| ``` | ||
|
|
||
| ### Ad-hoc compaction outside the provider pipeline | ||
|
|
||
| `CompactionProvider.CompactAsync` applies a strategy to an arbitrary list of messages without an active agent session: | ||
|
|
||
| ```csharp | ||
| IEnumerable<ChatMessage> compacted = await CompactionProvider.CompactAsync( | ||
| new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(8000)), | ||
| existingMessages); | ||
| ``` | ||
|
|
||
| ### Using a different model for summarization | ||
|
|
||
| The `SummarizationCompactionStrategy` accepts any `IChatClient`. Use a smaller, cheaper model to reduce summarization cost: | ||
|
|
||
| ```csharp | ||
| IChatClient summarizerChatClient = openAIClient.GetChatClient("gpt-4o-mini").AsIChatClient(); | ||
| new SummarizationCompactionStrategy(summarizerChatClient, CompactionTriggers.TokensExceed(4000)) | ||
| ``` | ||
|
|
||
| ### Registering through `ChatClientAgentOptions` | ||
|
|
||
| `CompactionProvider` can also be specified directly on `ChatClientAgentOptions` instead of calling `UseAIContextProviders` on the `ChatClientBuilder`: | ||
|
|
||
| ```csharp | ||
| AIAgent agent = agentChatClient | ||
| .AsBuilder() | ||
| .BuildAIAgent(new ChatClientAgentOptions | ||
| { | ||
| AIContextProviders = [new CompactionProvider(compactionPipeline)] | ||
| }); | ||
| ``` | ||
|
|
||
| This places the compaction provider at the agent level instead of the chat client level, which allows you to use different compaction strategies for different agents that share the same chat client. | ||
|
|
||
| > Note: In this mode the `CompactionProvider` is not engaged during the tool calling loop. Agent-level `AIContextProviders` run before chat history is stored, so any synthetic summary messages produced by `CompactionProvider` can become part of the persisted history when using `ChatHistoryProvider`. If you want to compact only the request context while preserving the original stored history, register `CompactionProvider` on the `ChatClientBuilder` via `UseAIContextProviders(...)` instead of on `ChatClientAgentOptions`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.