[Blazor][Components.AI] Add a finalization callback for active content block handlers
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 276
Description
## Background and Motivation
`ContentBlockHandler` is invoked for each model update while its emitted block remains active. A handler can return `Complete()` when a later update indicates that its block has ended.
At the end of a response stream, however, `BlockMappingPipeline.Finalize()` marks all active blocks inactive without invoking their handlers. A handler therefore has no opportunity to flush buffered state when the stream ends.
This prevents efficient handlers that intentionally defer expensive work. For example, a Markdown handler might accumulate text fragments and periodically rebuild its parsed `RichTextNode` tree. Without a final callback, the handler cannot guarantee that the final buffered fragments are parsed unless it reparses the entire accumulated response after every update.
The same limitation can affect other buffered transformations, such as incremental protocol parsing, batched activity updates, or coalesced media metadata.
## Proposed API
Add a lifecycle callback that is invoked immediately before the pipeline deactivates an active block:
```diff
namespace Microsoft.AspNetCore.Components.AI;
public abstract class ContentBlockHandler
where TState : new()
{
public abstract BlockMappingResult Handle(
BlockMappingContext context,
TState state);
+ public virtual void OnFinalizing(TState state)
+ {
+ }
}
```
`BlockMappingPipeline.Finalize()` would invoke the callback for each active handler, notify the emitted block if its state changed, and then mark the block inactive.
## Usage Examples
```csharp
internal sealed class MarkdownContentHandler
: ContentBlockHandler
{
public sealed class State
{
public RichContentBlock Block { get; } = new();
public StringBuilder Text { get; } = new();
public bool HasUnparsedText { get; set; }
}
public override BlockMappingResult Handle(
BlockMappingContext context,
State state)
{
// Accumulate fragments and rebuild the parsed content periodically.
}
public override void OnFinalizing(State state)
{
if (state.HasUnparsedText)
{
var text = state.Text.ToString();
state.Block.ReplaceContent(text, MarkdownParser.Parse(text));
}
}
}
```
This allows a handler to throttle expensive transformations while guaranteeing that its emitted block contains the final state when the response completes.
## Alternative Designs
- Pass a synthetic end-of-stream update through active handlers.
- Add an `IsFinal` property to `BlockMappingContext`.
- Add a pipeline-level completion event.
- Make finalization asynchronous with `OnFinalizingAsync`.
A synchronous callback is sufficient for in-memory parsing and avoids making the pipeline finalization path asynchronous. An asynchronous variant could be considered if handlers are expected to perform I/O.
## Risks
- A finalization callback could add latency at the end of a response.
- Exceptions need clearly defined behavior.
- The pipeline must notify the emitted block after finalization so the UI observes the last mutation.
- The callback should be invoked exactly once, including cancellation and error paths if those paths finalize blocks.
Contributor guide
Assessment
This issue has not been assessed yet.