e-naxosMAF COURSE / COURS
Français

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

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
Composition and paths from repository root
MechanismScope
demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csprojAgent, HTTP 5088 and durable sessions
demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csprojContracts, NDJSON reader and Toolkit ViewModel
demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csprojTerminal client; Ctrl+C
demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csprojRazor shell 5089; JavaScript → backend
demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csprojWindows 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-build

Compare 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.");

demos/Demo410_StreamingInterfaces/Client/DiscussionClient.cs · L21–L37 · 9a895c94534c

Source code · Demo410 · Folder

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-restore

ExactWhitespaceAndUnicodeSurviveHttpAndSessionRestart 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}"; }

demos/Demo410_StreamingInterfaces/Client/DiscussionViewModel.cs · L24–L40 · 9a895c94534c

Source code · Demo410 · Folder

public MainWindow()
{
    InitializeComponent();
    var viewModel = new DiscussionViewModel(new DiscussionClient(http));
    DataContext = viewModel;
    Closed += (_, _) => { viewModel.SendCommand.Cancel(); http.Dispose(); };

demos/Demo410_StreamingInterfaces/Wpf/MainWindow.xaml.cs · L17–L22 · 9a895c94534c

Source code · Demo410 · Folder

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();

demos/Demo410_StreamingInterfaces/Web/Program.cs · L3–L14 · 9a895c94534c

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; }

demos/Demo410_StreamingInterfaces/Console/Program.cs · L10–L20 · 9a895c94534c

Source code · Demo410 · Folder

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);

demos/Demo410_StreamingInterfaces/Backend/Backend.cs · L57–L79 · 9a895c94534c

Source code · Demo410 · Folder

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);

demos/Demo410_StreamingInterfaces/Backend/ConversationEngine.cs · L54–L59 · 9a895c94534c

Source code · Demo410 · Folder

Prerequisites

Continue reading

Complete demo folder · README

lot011-interfaces-1 · 2026-10-24 · 9a895c94534cd5fc6f3da883e37e5f7856e588c1