MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1
Demo410 — Streaming Interfaces
Start the shared backend then its clients; check stream completion, context resumption and cancellation limits.
Objectives
- Use a shared streaming backend from console, Web and WPF clients with persistent sessions.
Prepare the multi-project distribution
Use the complete solution built in Release and an actually available chat provider. Shared ai-settings.json is copied beside the backend; any keys remain server-side. No embeddings, RAG or Docker are required for these commands. WPF requires Windows with the .NET desktop runtime. Also retain Demo400.Server for /incident.
dotnet build BaseTechnique.slnx -m:1 -c Release| Mechanism | Scope |
|---|---|
| demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj | Agent, HTTP 5088 and durable sessions |
| demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj | Contracts, NDJSON reader and Toolkit ViewModel |
| demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj | Terminal client; Ctrl+C |
| demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj | Razor shell 5089; JavaScript → backend |
| demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj | Windows window; MVVM binding |
Startup order and ports
Terminal 1, at repository root: start the backend. Choose a state directory dedicated to this experiment and retain it for resumption; the command below reuses maf-demo410-lab. Do not run two backends against that directory. The local provider must already be available.
dotnet run --project demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj -c Release --no-build --no-launch-profile -- --urls http://localhost:5088 --Conversation:StateRoot (Join-Path $env:TEMP "maf-demo410-lab")Terminal 2: configure the console client and send the request. The backend starts the MCP server itself for /incident, receives the actual fixture result and supplies it as untrusted model context. No additional MCP server needs manual startup.
$env:MAF_BACKEND_URL = 'http://localhost:5088'dotnet run --project demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj -c Release --no-build -- 0123456789abcdef0123456789abcdef "/incident INC-318"Optional terminal 3: start the Web shell then open http://localhost:5089. The browser calls http://localhost:5088 directly. Optional terminal 4: set MAF_BACKEND_URL in that terminal then start WPF. --no-launch-profile and --urls fix ports independently of the Visual Studio profile.
dotnet run --project demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj -c Release --no-build --no-launch-profile -- --urls http://localhost:5089 --Backend:Url=http://localhost:5088$env:MAF_BACKEND_URL = 'http://localhost:5088'dotnet run --project demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj -c Release --no-buildCompare complete display and streaming
In Web, use Send complete then Send stream with a fresh ID for each independent comparison. The former waits for the complete answer; the latter appends text progressively; both save after success. WPF and console use streaming. Two actual model calls can produce different text: byte-for-byte equality must be measured with test fixtures rather than confusing model variation with lost whitespace.
using var response = await http.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(token);
using var reader = new StreamReader(stream);
while (await reader.ReadLineAsync(token) is { } line)
{
var frame = JsonSerializer.Deserialize<StreamEvent>(line, JsonSerializerOptions.Web)
?? throw new InvalidDataException("Invalid stream event.");
switch (frame.Type)
{
case "text": onText(frame.Text ?? throw new InvalidDataException("Missing text.")); break;
case "completed": return;
case "error": throw new InvalidOperationException(frame.Text ?? "Generation failed.");
default: throw new InvalidDataException("Unknown stream event.");
}
}
throw new EndOfStreamException("The response ended before completion; completion is unconfirmed.");
Source code · Demo410 · Folder
- demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj
- demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj
- demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj
- demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj
- demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj
Send INC-999 to observe absent data, then a long request to try Cancel. Check Generating, Completed and saved or completion unconfirmed labels. Stopping the backend during a response must leave failure or unconfirmed completion, never success inferred from already visible text. A disconnect after commit may nevertheless have saved the turn.
Resume after restart and check tests
dotnet run --project demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj -c Release --no-build -- 0123456789abcdef0123456789abcdef "Remember the marker BLUE-731 for our next turn."Wait for Completed and saved, stop then restart the backend with the same state directory and provider. Reuse exactly the previous ID for the question below. Then compare with a new ID. Context presence is session state; a correct model answer remains a separate semantic observation. Restart does not reconstruct visual history.
dotnet run --project demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj -c Release --no-build -- 0123456789abcdef0123456789abcdef "Which marker did we discuss?"dotnet test tests/Streaming.Tests/Streaming.Tests.csproj -m:1 -c Release --no-build --no-restoreExactWhitespaceAndUnicodeSurviveHttpAndSessionRestart checks exact text and resumption with a scripted model. MidstreamErrorIsVisibleAndDoesNotCommitPartialTurn and ClientCancellationReachesModelAndDoesNotSave protect the previous snapshot. Toolkit tests check commands and observable messages. RealMcpLookupSuppliesActualIncidentDataToAgent uses the actual stdio server. These successes do not constitute graphical acceptance.
[RelayCommand(IncludeCancelCommand = true)]
private async Task SendAsync(CancellationToken token)
{
if (string.IsNullOrWhiteSpace(Input)) return;
var prompt = Input;
Input = "";
Messages.Add(new("User") { Text = prompt });
var answer = new DisplayMessage("Assistant");
Messages.Add(answer);
Status = "Generating";
try
{
await client.StreamAsync(new(SessionId, prompt), text => answer.Text += text, token);
Status = "Completed and saved";
}
catch (OperationCanceledException) when (token.IsCancellationRequested) { Status = "Canceled; completion unconfirmed. Partial display only."; }
catch (Exception error) { Status = $"Failed; completion unconfirmed. {error.Message}"; }
Source code · Demo410 · Folder
- demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj
- demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj
- demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj
- demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj
- demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj
public MainWindow()
{
InitializeComponent();
var viewModel = new DiscussionViewModel(new DiscussionClient(http));
DataContext = viewModel;
Closed += (_, _) => { viewModel.SendCommand.Cancel(); http.Dispose(); };
Source code · Demo410 · Folder
- demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj
- demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ContentRootPath = AppContext.BaseDirectory
});
builder.Configuration.AddDemoConfiguration(args);
if (builder.Configuration["urls"] is null) builder.WebHost.UseUrls("http://localhost:5089");
builder.Services.AddRazorPages();
var app = builder.Build();
app.UseStaticFiles();
app.MapRazorPages();
app.Run();
Source code · Demo410 · Folder
var session = args.Length > 0 ? args[0] : Guid.NewGuid().ToString("N");
Console.WriteLine($"Session: {session}. Reuse this ID to resume. Ctrl+C cancels.");
try
{
var prompt = args.Length > 1 ? args[1] : "/incident INC-318";
await new DiscussionClient(http).StreamAsync(new(session, prompt), Console.Write, stop.Token);
Console.WriteLine("\nCompleted and saved.");
return 0;
}
catch (OperationCanceledException) when (stop.IsCancellationRequested) { Console.Error.WriteLine("Canceled."); return 130; }
catch (Exception error) { Console.Error.WriteLine($"Failed: {error.Message}"); return 2; }
Execution and acceptance reservations
The setup is local, single-process and unauthenticated. An ID is not access permission. Reconnection means making another request with the retained ID; there is no automatic stream or turn replay. New conversation does not delete old snapshots. Cancellation does not undo a successful commit. Model quality and interactive Web/WPF acceptance are assessed separately from backend tests.
Acceptance on 24 October 2026: the Razor shell and its JavaScript were exercised in Edge with five explicitly scripted responses, including truncation, error and cancellation. This check is not a Web → backend → real model chain. The WPF window could not be exercised: Computer Use failed before application access. The 16 streaming tests, including ViewModel tests, and 22 MCP tests passed separately. These HTTP endpoints do not expose the integrated durable approval scenario.
Follow each component’s files
await engine.RunAsync(request, async fragment =>
{
if (streaming)
{
if (!context.Response.HasStarted)
{
context.Response.ContentType = "application/x-ndjson; charset=utf-8";
context.Response.Headers.CacheControl = "no-store";
}
await WriteEventAsync(context, new("text", fragment), token);
}
else text.Append(fragment);
}, token);
if (streaming)
{
if (!context.Response.HasStarted)
{
context.Response.ContentType = "application/x-ndjson; charset=utf-8";
context.Response.Headers.CacheControl = "no-store";
}
await WriteEventAsync(context, new("completed"), token);
}
else await context.Response.WriteAsJsonAsync(new { message = text.ToString() }, token);
Source code · Demo410 · Folder
- demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj
- demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj
- demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj
- demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj
- demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj
await foreach (var update in agent.RunStreamingAsync(prompt, session, cancellationToken: token))
{
if (!string.IsNullOrEmpty(update.Text)) await onText(update.Text);
}
token.ThrowIfCancellationRequested();
await store.SaveAsync(agent, session, token);
Source code · Demo410 · Folder
- demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csproj
- demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csproj
- demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csproj
- demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csproj
- demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csproj
