e-naxosMAF COURSE / COURS
English

MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1

Demo410 — Streaming Interfaces

Démarrer le backend commun puis les clients ; vérifier la fin du flux, la reprise du contexte et les limites de l’annulation.

Objectifs

Préparer la distribution multi-projets

Utiliser la solution complète compilée en Release et un fournisseur de chat réellement disponible. La configuration commune ai-settings.json est copiée à côté du backend ; les clés éventuelles restent côté serveur. Aucun embedding, RAG ou Docker n’est requis pour ces commandes. WPF exige Windows avec le runtime desktop .NET. Conserver aussi Demo400.Server pour /incident.

dotnet build BaseTechnique.slnx -m:1 -c Release
Composition et chemins depuis la racine du dépôt
MécanismePortée
demos/Demo410_StreamingInterfaces/Backend/Demo410.Backend.csprojAgent, HTTP 5088 et sessions durables
demos/Demo410_StreamingInterfaces/Client/Demo410.Client.csprojContrats, lecteur NDJSON et ViewModel Toolkit
demos/Demo410_StreamingInterfaces/Console/Demo410.Console.csprojClient terminal ; Ctrl+C
demos/Demo410_StreamingInterfaces/Web/Demo410.Web.csprojShell Razor 5089 ; JavaScript → backend
demos/Demo410_StreamingInterfaces/Wpf/Demo410.Wpf.csprojFenêtre Windows ; binding MVVM

Ordre de démarrage et ports

Terminal 1, à la racine du dépôt : démarrer le backend. Choisir un dossier d’état consacré à cet essai et le conserver pour la reprise ; la commande ci-dessous réutilise maf-demo410-lab. Ne pas lancer deux backends sur ce dossier. Le fournisseur local doit déjà être disponible.

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 : configurer le client console puis envoyer la demande. Le backend lance lui-même le serveur MCP pour /incident, reçoit le vrai résultat de fixture et le fournit comme contexte non fiable au modèle. Aucun serveur MCP supplémentaire à démarrer manuellement.

$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"

Terminal 3 facultatif : démarrer le shell Web puis ouvrir http://localhost:5089. Le navigateur appelle directement http://localhost:5088. Terminal 4 facultatif : définir MAF_BACKEND_URL dans ce terminal puis lancer WPF. --no-launch-profile et --urls fixent les ports sans dépendre du profil Visual Studio.

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

Comparer affichage complet et flux

Dans Web, utiliser Send complete puis Send stream avec un ID neuf pour chaque comparaison indépendante. Le premier attend la réponse complète, le second ajoute le texte progressivement ; tous deux sauvegardent après succès. WPF et console utilisent le flux. Deux appels réels au modèle peuvent produire des textes différents : l’égalité octet pour octet doit être mesurée avec les fixtures des tests, sans confondre variation du modèle et perte d’espaces.

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

Code source · Demo410 · Dossier

Envoyer INC-999 pour observer l’absence de donnée, puis un message long pour essayer Cancel. Vérifier les libellés Generating, Completed and saved ou completion unconfirmed. Arrêter le backend en cours de réponse doit laisser un échec ou une fin non confirmée, jamais un succès déduit du texte déjà visible. Une coupure après commit peut toutefois avoir sauvegardé le tour.

Reprendre après redémarrage et vérifier les 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."

Attendre Completed and saved, arrêter puis relancer le backend avec le même dossier d’état et le même fournisseur. Réutiliser exactement l’ID précédent pour la question ci-dessous. Comparer ensuite avec un nouvel ID. La présence du contexte est un état de session ; la réponse correcte du modèle reste une observation sémantique distincte. Le redémarrage ne reconstruit pas l’historique visuel.

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 vérifie le texte exact et la reprise avec un modèle scripté. MidstreamErrorIsVisibleAndDoesNotCommitPartialTurn et ClientCancellationReachesModelAndDoesNotSave protègent le snapshot précédent. Les tests Toolkit vérifient les commandes et les messages observables. RealMcpLookupSuppliesActualIncidentDataToAgent utilise le vrai serveur stdio. Ces succès ne constituent pas une recette graphique.

[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

Code source · Demo410 · Dossier

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

Code source · Demo410 · Dossier

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

Code source · Demo410 · Dossier

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

Code source · Demo410 · Dossier

Réserves d’exécution et de recette

Le montage est local, mono-processus et sans authentification. Un ID n’est pas un droit d’accès. La reconnexion consiste à refaire une demande avec l’ID conservé ; aucun rejeu automatique du flux ou du tour. Ne pas effacer les anciens snapshots en cliquant New conversation. L’annulation n’annule pas un commit déjà réussi. La qualité du modèle et la recette interactive Web/WPF sont évaluées séparément des tests backend.

Recette du 24 octobre 2026 : le shell Razor et son JavaScript ont été exercés dans Edge avec cinq réponses explicitement scriptées, dont troncature, erreur et annulation. Ce contrôle n’est pas une chaîne Web → backend → modèle réel. La fenêtre WPF n’a pas pu être exercée : Computer Use a échoué avant accès à l’application. Les 16 tests streaming, dont ceux du ViewModel, et les 22 tests MCP ont réussi séparément. Le scénario d’approbation durable intégré n’est pas exposé par ces endpoints HTTP.

Suivre les fichiers de chaque composant

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

Code source · Demo410 · Dossier

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

Code source · Demo410 · Dossier

Prérequis

Pour poursuivre

Dossier complet de la démo · README

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