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;publicstaticclass Harness{publicstaticreadonlystring RepoRoot =FindRepoRoot(Directory.GetCurrentDirectory());staticstringFindRepoRoot(string dir)=> File.Exists(Path.Combine(dir,"COURSE_CATALOG.generated.json"))? dir: dir.Length>3?FindRepoRoot(Path.GetDirectoryName(dir)!):thrownewInvalidOperationException("racine du depot introuvable");publicstaticstringRel(string path)=> Path.GetRelativePath(RepoRoot, path);publicstaticvoidShowFile(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]}");}publicstaticintDotnet(string args,string workdir){var psi =newProcessStartInfo("dotnet", args){ WorkingDirectory = Path.Combine(RepoRoot, workdir), RedirectStandardOutput =true, RedirectStandardError =true, UseShellExecute =false,};var sb =newStringBuilder();var p = Process.Start(psi)!; p.OutputDataReceived+=(_, e)=>{if(e.Datais not null)lock(sb) sb.AppendLine(e.Data);}; p.ErrorDataReceived+=(_, e)=>{if(e.Datais 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"))}");
The below script needs to be able to find the current output cell; this is an easy method to get it.
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 :
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.
--- 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");
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.
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.
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).