Aspire : le harness Copilot SDK — CopilotClient, session, SessionEvent en C#

La série The Unexpected AI Stack a déjà livré ses couches : orchestration Aspire (01, 02), observabilité Serilog/OTEL (03), transport agent Channels/SSE (04), tests d’intégration Testcontainers (05), garde-fous Roslyn (06). Il manquait le maillon de tête : le harness lui-même. GitHub.Copilot.SDK 1.0.13 — publié par GitHub, le même moteur que les Copilot coding agents — met l’agent complet (runtime, session, outils, permissions, cycle de vie événementiel) derrière une API .NET.

Source de l’axe : Part 3 de la série chrlschn.dev (2026-08-14), distillée dans la veille #10475. Notre apport propre : le harness exécuté pour de vrai depuis .NET 10 — auth réelle, catalogue réel, tour de conversation réel, cycle de vie SessionEvent observé — pas une narration.

La Part 3 de la source branche ce harness sur Channels + SSE ; ce transport est déjà couvert par le notebook 04 — ici on montre la source du flux. Voir #10473, #10475.

A1 — Le package et son runtime embarqué

Le package NuGet pèse ~58 Mo : il embarque son runtime (bibliothèque native FFI). Aucune CLI externe à installer, aucun service à démarrer — CopilotClient.StartAsync() suffit. Les dépendances déclarées sont légères (Microsoft.Extensions.AI.Abstractions, System.Text.Json) : tout le poids est le runtime agent.

// Cellule d'amorcage : helper d'execution du notebook (chemins RELATIFS uniquement).
using System.Diagnostics;
using System.IO;
using System.Text;

public static class Harness
{
    public static readonly string RepoRoot = FindRepoRoot(Directory.GetCurrentDirectory());

    static string FindRepoRoot(string dir) =>
        File.Exists(Path.Combine(dir, "COURSE_CATALOG.generated.json")) ? dir
        : dir.Length > 3 ? FindRepoRoot(Path.GetDirectoryName(dir)!)
        : throw new InvalidOperationException("racine du depot introuvable");

    public static string Rel(string path) => Path.GetRelativePath(RepoRoot, path);

    public static void ShowFile(string relPath, int startLine = 1, int? endLine = null)
    {
        var abs = Path.Combine(RepoRoot, relPath);
        Console.WriteLine($"--- {relPath} ---");
        var lines = File.ReadAllLines(abs);
        var end = Math.Min(endLine ?? lines.Length, lines.Length);
        for (var i = startLine - 1; i < end; i++)
            Console.WriteLine($"{i + 1,4} | {lines[i]}");
    }

    public static int Dotnet(string args, string workdir)
    {
        var psi = new ProcessStartInfo("dotnet", args)
        {
            WorkingDirectory = Path.Combine(RepoRoot, workdir),
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        };
        var sb = new StringBuilder();
        var p = Process.Start(psi)!;
        p.OutputDataReceived += (_, e) => { if (e.Data is not null) lock (sb) sb.AppendLine(e.Data); };
        p.ErrorDataReceived += (_, e) => { if (e.Data is not null) lock (sb) sb.AppendLine(e.Data); };
        p.BeginOutputReadLine();
        p.BeginErrorReadLine();
        p.WaitForExit();
        // Projection d'affichage : les resumes de build embarquent le chemin absolu
        // de la DLL -- on retire les lignes qui le portent (meme hygiene que le notebook 05).
        var cleaned = string.Join(Environment.NewLine,
            sb.ToString().Split(Environment.NewLine).Where(l => !l.Contains(RepoRoot)));
        Console.WriteLine(cleaned);
        return p.ExitCode;
    }
}
Console.WriteLine($"amorcage OK -- projet compagnon : {Harness.Rel(Path.Combine(Harness.RepoRoot, "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App"))}");
amorcage OK -- projet compagnon : MyIA.AI.Notebooks\GenAI\Integrations-DotNet\Aspire\CopilotHarness.App

Lecture : L’amorçage a réussi et a créé le projet compagnon CopilotHarness.App dans l’arborescence. Ce projet .NET 10 sera utilisé pour démontrer l’intégration du SDK GitHub Copilot.

Voici la feuille de dépendances du projet compagnon — elle tient en un fichier :

Harness.ShowFile("MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App/CopilotHarness.App.csproj");
--- MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App/CopilotHarness.App.csproj ---
   1 | <Project Sdk="Microsoft.NET.Sdk">
   2 | 
   3 |   <PropertyGroup>
   4 |     <OutputType>Exe</OutputType>
   5 |     <TargetFramework>net10.0</TargetFramework>
   6 |     <ImplicitUsings>enable</ImplicitUsings>
   7 |     <Nullable>enable</Nullable>
   8 |   </PropertyGroup>
   9 | 
  10 |   <ItemGroup>
  11 |     <PackageReference Include="GitHub.Copilot.SDK" Version="1.0.13" />
  12 |   </ItemGroup>
  13 | 
  14 | </Project>

Lecture : Le fichier CopilotHarness.App.csproj montre la configuration du projet : ciblage .NET 10.0, avec la référence au package GitHub.Copilot.SDK version 1.0.13. Ce SDK embarque son propre runtime natif (FFI), aucune CLI externe n’est nécessaire.

Interprétation — la dépendance du .csproj

Le .csproj ne déclare qu’une dépendance inhabituelle : GitHub.Copilot.SDK 1.0.13. C’est la seule ligne qui change tout — le runtime agent, ses outils et sa couche de permissions arrivent avec le package. Le reste est un net10.0 ordinaire.

// Compilation du projet compagnon (les cellules suivantes lancent avec --no-build).
Harness.Dotnet("build", "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App");
  Identification des projets à restaurer...
  Tous les projets sont à jour pour la restauration.

La génération a réussi.
    0 Avertissement(s)
    0 Erreur(s)

Temps écoulé 00:00:01.37

Lecture : Le fichier Program.cs est le point d’entrée du harness. Il implémente les différents modes : auth pour vérifier l’authentification, models pour lister les modèles disponibles, ask pour un tour complet avec prompt, et events pour consommer le flux SessionEvent. Chaque mode démontre une capacité différente du SDK Copilot.

A2 — CopilotClient : démarrer le harness, l’auth qui se propage

Program.cs construit un CopilotClientOptions (le répertoire de travail délimite le terrain d’action de l’agent), démarre le client, puis propose quatre modes : auth, models, ask, events.

Harness.ShowFile("MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App/Program.cs", 1, 40);
--- MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App/Program.cs ---
   1 | // CopilotHarness.App : le harness GitHub.Copilot.SDK exécute pour de vrai.
   2 | // Le package embarque son runtime (FFI native) : aucune CLI externe à installer.
   3 | // Modes :
   4 | //   auth            -- état d'authentification du harness (aucun appel modèle)
   5 | //   models          -- catalogue des modèles servis par Copilot (aucun appel modèle)
   6 | //   ask "<prompt>"  -- un tour complet : session + envoi + attente de la réponse
   7 | //   events "<prompt>" -- même tour, mais en consommant le flux SessionEvent au fil de l'eau
   8 | using System.Text;
   9 | using System.Text.Json;
  10 | using GitHub.Copilot;
  11 | 
  12 | var mode = args.Length > 0 ? args[0] : "auth";
  13 | if (mode is "ask" or "events" && args.Length < 2)
  14 | {
  15 |     Console.WriteLine($"usage : CopilotHarness.App {mode} \"<prompt>\"");
  16 |     return 1;
  17 | }
  18 | var jsonOpts = new JsonSerializerOptions { WriteIndented = true };
  19 | 
  20 | var options = new CopilotClientOptions
  21 | {
  22 |     // Le harness est un agent : son répertoire de travail délimite son terrain d'action.
  23 |     WorkingDirectory = Environment.CurrentDirectory,
  24 | };
  25 | using var client = new CopilotClient(options);
  26 | await client.StartAsync();
  27 | 
  28 | switch (mode)
  29 | {
  30 |     case "auth":
  31 |     {
  32 |         var auth = await client.GetAuthStatusAsync();
  33 |         Console.WriteLine(JsonSerializer.Serialize(auth, jsonOpts));
  34 |         break;
  35 |     }
  36 |     case "models":
  37 |     {
  38 |         var models = await client.ListModelsAsync();
  39 |         Console.WriteLine($"{"id",-24} {"nom",-22} vision");
  40 |         foreach (var m in models.OrderBy(m => m.Id, StringComparer.Ordinal))

Première exécution réelle : l’état d’authentification du harness sur cette machine.

// Le mode auth : aucun appel modele, juste l'etat du harness sur cette machine.
Harness.Dotnet("run --no-build -- auth", "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App");
{
  "isAuthenticated": true,
  "authType": "gh-cli",
  "host": "https://github.com",
  "login": "jsboige",
  "statusMessage": "jsboige (via gh)"
}

Lecture : Le statut d’authentification confirme que l’utilisateur jsboige est authentifié via gh-cli sur https://github.com. Le harness peut maintenant interagir avec l’API GitHub Copilot.

Interprétation — une authentification héritée, sans secret

isAuthenticated: true, authType: "gh-cli" : le harness hérite l’authentification de la machine — ici la session gh locale — sans qu’aucune clé ne figure dans le code ou dans l’environnement du projet. Le SDK propose aussi la voie BYOK (LlmInferenceAdapter, échange LlmInferenceExchange) pour déporter l’inférence vers votre propre endpoint ; elle n’est pas exercée ici et reste documentée par sa signature publique.

Aucun secret ne transite dans les sorties : l’objet d’auth expose un état, pas un jeton.

A3 — Le catalogue : quinze modèles derrière une API .NET

ListModelsAsync rend le catalogue servi par Copilot — capacités (vision, effort de raisonnement), fenêtres de contexte et facturation inclus. C’est la surface qui permet de choisir un modèle programmatiquement plutôt que de le figer en configuration.

Harness.Dotnet("run --no-build -- models", "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App");
id                       nom                    vision
auto                     Auto                   non
claude-haiku-4.5         Claude Haiku 4.5       oui
claude-sonnet-5          Claude Sonnet 5        oui
gpt-5-mini               GPT-5 mini             oui
gpt-5.3-codex            GPT-5.3-Codex          oui
gpt-5.4                  GPT-5.4                oui
gpt-5.4-mini             GPT-5.4 mini           oui
gpt-5.6-luna             GPT-5.6 Luna           oui
gpt-5.6-terra            GPT-5.6 Terra          oui
grok-4.5                 Grok 4.5               oui
grok-4.6                 Grok 4.6               oui
kimi-k2.7-code           Kimi K2.7 Code         oui
kimi-k3                  Kimi K3                oui
mai-code-1-flash-picker  MAI-Code-1-Flash       non
mai-code-1.1-flash       MAI-Code-1.1-Flash     oui
TOTAL 15 modeles

Lecture : Le catalogue des 15 modèles disponibles est affiché, avec pour chacun l’identifiant, le nom et la capacité de vision. On note la présence de modèles Claude (Sonnet 5, Haiku 4.5), GPT-5 (mini, 5.3-codex, 5.4, 5.4-mini, 5.6-luna/terra), Grok (4.5, 4.6) et Kimi (K2.7 Code, K3), tous avec vision sauf Auto et MAI-Code-1-Flash.

Interprétation — quinze modèles derrière une seule API

Quinze modèles — Claude Sonnet 5, Haiku 4.5, la famille GPT-5.x, Grok, Kimi, MAI — derrière une seule API locale. L’entrée auto est le sélecteur par défaut : le harness choisit. Pour un notebook pédagogique, ce catalogue est aussi l’occasion d’un exercice LINQ (voir Exercice 3).

A4 — Un tour complet : session, envoi, réponse

CreateSessionAsync ouvre une conversation d’agent ; SendAndWaitAsync envoie un message en texte brut et attend que la session redevienne inactive. Comptez une requête premium Copilot par exécution des cellules ask/events de ce notebook — le prompt est volontairement minuscule.

Harness.Dotnet("run --no-build -- ask \"En une seule phrase de 12 mots maximum : que represente le package GitHub.Copilot.SDK pour un developpeur .NET ?\"", "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App");
session c339d194-6798-4fef-b022-ca67cfc4f1d7
{
  "data": {
    "apiCallId": "msg_011CekmS83YokuKFAsxzqFDr",
    "content": "Le SDK officiel .NET pour int\u00E9grer GitHub Copilot dans ses applications.",
    "interactionId": "4787e1cc-5747-4632-ba41-a739c3307857",
    "messageId": "9e27008d-aec5-4c0e-a66f-f026aa342b7a",
    "model": "claude-sonnet-5",
    "rte": true,
    "toolRequests": [],
    "turnId": "0"
  },
  "id": "6b9bb3a4-f56b-4741-96dd-92eeefd16424",
  "parentId": "1bba9bea-cd39-4abb-bcff-0af8598b638e",
  "timestamp": "2026-09-05T18:29:12.135+00:00"
}

Lecture : Au prompt « En une seule phrase de 12 mots maximum : que represente le package GitHub.Copilot.SDK pour un developpeur .NET ? », le modèle claude-sonnet-5 répond exactement : « Le SDK officiel .NET pour intégrer GitHub Copilot dans ses applications. » — avec un apiCallId unique. Le harness a géré la session complète : authentification, envoi de la requête, réception de la réponse.

Interprétation — un tour d’agent complet, pas un client LLM brut

La réponse arrive enrichie : model (ici claude-sonnet-5 — le sélecteur auto a tranché), toolRequests (vide : ce tour n’a déclenché aucun outil), apiCallId/turnId pour la traçabilité. Ce n’est pas un client LLM brut : c’est un tour d’agent complet, avec sa politique d’outils et ses identifiants de tour.

A5 — Le cycle de vie observable : les SessionEvent

Le mode events joue le même tour, puis relit GetEventsAsync() — qui rend la liste des événements de la session (un instantané IReadOnlyList<SessionEvent>, pas un flux IAsyncEnumerable). C’est la source que le notebook 04 transportait ensuite par Channels et SSE.

Harness.Dotnet("run --no-build -- events \"Reponds exactement : BONJOUR HARNESS\"", "MyIA.AI.Notebooks/GenAI/Integrations-DotNet/Aspire/CopilotHarness.App");
session 2f44ca21-89f0-41c0-9921-26a20675fb35
histogramme des SessionEvent du tour :
  assistant.message                        1
  assistant.turn_end                       1
  assistant.turn_start                     1
  session.model_change                     1
  session.start                            1
  session.usage_checkpoint                 1
  system.message                           1
  user.message                             1
texte assemble depuis les deltas (15 car.) :
BONJOUR HARNESS

Lecture : L’histogramme des SessionEvent montre les différentes étapes du tour : session.start, system.message, user.message, assistant.turn_start, assistant.message, assistant.turn_end, session.usage_checkpoint, session.model_change. Le texte assemblé à partir des deltas est BONJOUR HARNESS, confirmant le bon fonctionnement du flux.

Interprétation — le canal événementiel du tour

L’histogramme raconte le tour dans l’ordre : session.start → session.model_change (résolution de auto) → system.message → user.message → assistant.turn_start → assistant.message → assistant.turn_end → session.usage_checkpoint. Le texte BONJOUR HARNESS a été ré-assemblé depuis le champ content des événements d’assistant — la preuve que le contenu transite bien par le canal événementiel, pas seulement par la valeur de retour.

Exercices

Les trois exercices étendent Program.cs. À chaque fois : modifier le fichier, reconstruire (dotnet build), exécuter le mode nouveau — et compter les requêtes premium que votre test consomme.

Exercice 1 — le point de contrôle d’usage

Le mode usage : après un tour, extraire du session.usage_checkpoint le nombre de tokens consommés et l’afficher.

// EXERCICE 1 : le mode "usage".
// Objectif : ajouter un mode qui, apres un tour, extrait du session.usage_checkpoint
// le nombre de tokens du tour et l'affiche. Indice : serialiser l'evenement
// checkpoint en JSON et lire ses champs.
// TODO etudiant : implementer le mode puis l'appeler ici.
Console.WriteLine("Exercice a completer : mode usage (voir TODO dans Program.cs)");
Exercice a completer : mode usage (voir TODO dans Program.cs)

Exercice 2 — la mémoire de conversation

Le mode memoire : deux tours dans la même session, dont le second prouve que le premier est retenu.

// EXERCICE 2 : la memoire de conversation.
// Objectif : dans une MEME session, envoyer "Retiens le mot XYLOPHONE" puis
// "Quel mot te devais-je ?" et montrer que la seconde reponse cite le mot.
// Indice : SendAndWaitAsync deux fois sur la meme instance de session.
// TODO etudiant : implementer le mode "memoire" puis l'appeler ici.
Console.WriteLine("Exercice a completer : mode memoire (voir TODO dans Program.cs)");
Exercice a completer : mode memoire (voir TODO dans Program.cs)

Exercice 3 — le catalogue trié

Le mode catalogue : table markdown des modèles à vision, triés par fenêtre de contexte décroissante.

// EXERCICE 3 : le catalogue en table triee.
// Objectif : mode "catalogue" qui produit une table markdown des modeles
// capables de vision, triee par fenetre de contexte decroissante.
// Indice : ListModelsAsync puis OrderByDescending sur Capabilities.Limits.
// TODO etudiant : implementer le mode puis l'appeler ici.
Console.WriteLine("Exercice a completer : mode catalogue (voir TODO dans Program.cs)");
Exercice a completer : mode catalogue (voir TODO dans Program.cs)

Conclusion

Le maillon de tête de la pile est en place : un harness d’agent complet, local et scriptable — auth héritée de la machine, catalogue interrogeable, tours réels, cycle de vie observable. La série du dépôt couvre désormais la chaîne entière de l’article source : runtime encapsulé (01-02), télémétrie (03), transport de flux (04), tests éclatés (05), garde-fous statiques (06), et maintenant le harness agent lui-même.

Limites honnêtes : l’adaptateur BYOK (LlmInferenceAdapter) n’est pas exercé — il dépend d’un endpoint d’inférence dédié et reste à l’état de signature ; les exécutions consomment le quota premium de l’abonnement Copilot de la machine (deux requêtes par passage complet du notebook).

Voir #10473 (Epic), #10475 (veille).

Retour au sommet