Od zera do agenta AI w C#

Microsoft Agent Framework 1.0 i MCP w praktyce

Od zera do agenta AI w C#
Microsoft Agent Framework 1.0
i MCP w praktyce

@walenciukC

Speaker
Jakie są wyzwania w budowaniu systemu wieloagentowego?
  1. Jakie są wyzwania w budowaniu systemu wieloagentowego?
Czego potrzebujemy, aby mieć framework agentów?
  1. Czego potrzebujemy, aby mieć framework agentów?
Czyżbyśmy w końcu mieli to w .NET i C#?
Microsoft Agent Framework 1.0 został oficjalnie wydany 3 kwietnia 2026
  1. Microsoft Agent Framework 1.0
Semantic Kernel AutoGen Microsoft Agent Framework
Rola SDK do orkiestracji AI dla systemów enterprise Framework badawczy do systemów multi-agentowych Ujednolicona platforma agentowa klasy enterprise
Fundament Skills, pluginy, workflow i integracje Współpracujące agenty i konwersacyjna orkiestracja Połączenie orkiestracji enterprise i architektury multi-agentowej
Interoperacyjność Pluginy, konektory oraz wsparcie dla MCP A2A OpenAPI Elastyczna integracja agentów i narzędzi, rozwijające się wsparcie protokołów Wbudowane konektory, natywne wsparcie dla MCP A2A OpenAPI
Pamięć Abstrakcja pamięci i integracje z bazami wektorowymi Pamięć konwersacji oraz zewnętrzne magazyny pamięci Trwała, adaptacyjna i wielowarstwowa pamięć agentów
Orkiestracja Deterministyczna + dynamiczna (workflow, planners, process framework) Dynamiczna orkiestracja LLM (debata, reflection, group chat, facilitator/worker) Deterministyczna + dynamiczna orkiestracja agentów i workflow
Gotowość
enterprise
Telemetria, observability, compliance, integracja z Azure Mała, stworzona do badań Observability, approvals, CI/CD, durable execution, hydration
Grupa
docelowa
Zespoły enterprise budujące aplikacje AI Research i eksperymenty z agentami AI Zespoły budujące produkcyjne systemy agentowe
Ewolucja Fundament orkiestracji enterprise AI Fundament architektur multi-agent Połączenie Semantic Kernel + AutoGen w jeden ekosystem

Nie warto ślepo ufać marketingowym slajdom


W mojej opinii Semantic Kernel miał wiele problemów architektonicznych


Microsoft Agent Framework to naprawdę dojrzały framework produkcyjny

Pod koniec prezentacji pokażę ponad 10 powodów, dlaczego Microsoft Agent Framework jest lepszy

Co to jest agent?
screenshot
screenshot
Kiedy potrzebujesz systemu wieloagentowego?
screenshot
screenshot
screenshot

Jak wywołać OpenAPI REST w C#

                    
                        using Microsoft.Extensions.AI;
                        using OllamaSharp;

                        // Klient Ollama : domyślnie http://localhost:11434
                        var ollama = new OllamaApiClient("http://localhost:11434");

                        // IChatClient z Microsoft.Extensions.AI
                        IChatClient client = ollama
                            .AsChatClient("qwen3:8b");

                        var response = await client.GetResponseAsync(
                            "Co to jest Model Context Protocol?"
                        );

                        Console.WriteLine(response.Text);

                        
                    
                

Jakie paczki NuGet?

                    
                        <PackageReference Include="Microsoft.Extensions.AI" 
                        Version="10.5.2" />

                        <PackageReference Include="OllamaSharp"
                        Version="5.*" />
                    
                

Jak wywołać OpenAPI REST w C# z historią czatu

                    
                        using Microsoft.Extensions.AI;
                        using OllamaSharp;

                        IChatClient client = new OllamaApiClient("http://localhost:11434")
                            .AsChatClient("qwen3:8b");

                        // Historia czatu : lista wiadomości
                        var history = new List<ChatMessage>
                        {
                            new(ChatRole.System, "Jesteś pomocnym asystentem .NET.")
                        };

                        while (true)
                        {
                            Console.Write("\nTy: ");
                            var input = Console.ReadLine();
                            if (string.IsNullOrWhiteSpace(input)) break;

                            history.Add(new(ChatRole.User, input));

                            // Przekazujemy całą historię : model "pamięta" kontekst
                            var response = await client.GetResponseAsync(history);

                            Console.WriteLine($"\nModel: {response.Text}");

                            // Dodajemy odpowiedź modelu do historii
                            history.Add(response.Message);
                        }
                    
                

Streamowanie odpowiedzi zwracanej przez OpenAPI

                    
                        await foreach (var update in
                            client.
                                GetStreamingResponseAsync(history))
                        {
                            Console.Write(update.Text);
                        }
                    
                
screenshot
screenshot
screenshot
screenshot
Oto przykład, jak coś takiego napisać od zera

Oto przykład mojego kodu, który uruchamia czat i podpina Playwright MCP do testowania strony onboardingowej TalentFlow.

Kod jest na GitHubie, link pod koniec prezentacji.

screenshot
screenshot

Program.cs - przykład połączenia LLM z Playwright MCP, co tworzy jednego agenta

                    
                        using Microsoft.Extensions.AI;
                        using ModelContextProtocol.Client;
                        using OpenAI;
                        using PlaywrightMcpDemo;
                        using System.ClientModel;
                        using System.Text;
                        using System.Text.Json;

                        const string OLLAMA_URL = "http://localhost:11434/v1";
                        const string OLLAMA_MODEL = "qwen3:14b";
                        const string DEMO_PAGE_URL = "http://localhost:5500/talentflow-onboarding.html";
                        const int SCENARIO_TIMEOUT_MINUTES = 20;

                        var scenario = (args.FirstOrDefault() ?? "A").ToUpperInvariant();
                        var headless = args.Contains("--headless");
                        var useClaude = args.Contains("--claude");

                        var titles = new Dictionary<string, string>
                        {
                            ["A"] = "Scenariusz A  -  Autonomiczny Agent HR",
                            ["B"] = "Scenariusz B  -  Generator Testow E2E",
                            ["C"] = "Scenariusz C  -  Audytor Dostepnosci WCAG",
                        };

                        if (!titles.TryGetValue(scenario, out var title))
                        {
                            Console.WriteLine("Uzycie: dotnet run -- [A|B|C] [--headless] [--claude]");
                            return;
                        }

                        ConsoleUi.WriteBanner(title);

                        // ── Sprawdz strone demo ──────────────────────────────────────
                        Console.Write($"Sprawdzam {DEMO_PAGE_URL} ... ");
                        if (!await DemoPageChecker.IsAvailableAsync(DEMO_PAGE_URL))
                        {
                            ConsoleUi.WriteError("Strona demo niedostepna!");
                            Console.WriteLine("Uruchom serwer: npx serve wwwroot -l 5500");
                            Console.WriteLine("              lub: python -m http.server 5500 --directory wwwroot");
                            return;
                        }
                        ConsoleUi.WriteOk("strona dostepna");

                        // ── Playwright MCP ───────────────────────────────────────────
                        Console.WriteLine("Uruchamianie Playwright MCP Server...");

                        var (command, arguments) = PlaywrightProcessResolver.Resolve(headless);
                        Console.WriteLine($"Uruchamianie: {command} {string.Join(" ", arguments)}");

                        await PlaywrightProcessResolver.VerifyAsync(command, arguments);

                        var transport = new StdioClientTransport(new StdioClientTransportOptions
                        {
                            Name = "playwright",
                            Command = command,
                            Arguments = arguments,
                        });
                        await using var mcp = await McpClient.CreateAsync(transport);
                        IList<McpClientTool> mcpTools = await mcp.ListToolsAsync();
                        ConsoleUi.WriteOk($"Playwright MCP gotowy ({mcpTools.Count} narzedzi)");

                        // ── Model LLM ────────────────────────────────────────────────
                        var modelName = useClaude ? "claude-haiku-4-5 (Anthropic API)" : $"{OLLAMA_MODEL} (Ollama)";

                        IChatClient innerClient = useClaude
                            ? new Anthropic.AnthropicClient()
                                .AsIChatClient("claude-haiku-4-5")
                            : new OpenAIClient(
                                    new ApiKeyCredential("ollama"),
                                    new OpenAIClientOptions { Endpoint = new Uri(OLLAMA_URL) })
                                .GetChatClient(OLLAMA_MODEL)
                                .AsIChatClient();

                        IChatClient chat = new ChatClientBuilder(innerClient)
                            .UseFunctionInvocation()
                            .Build();

                        ConsoleUi.WriteOk($"Model: {modelName}");
                        Console.WriteLine();

                        // ── Prompty ──────────────────────────────────────────────────
                        var (systemPrompt, userPrompt) = await PromptLoader.LoadAsync(scenario, DEMO_PAGE_URL);

                        // ── Agent ────────────────────────────────────────────────────
                        var messages = new List<ChatMessage>
                        {
                            new(ChatRole.System, systemPrompt),
                            new(ChatRole.User,   userPrompt),
                        };

                        var chatOptions = new ChatOptions
                        {
                            Tools = [.. mcpTools],
                            MaxOutputTokens = 16384,
                        };

                        Console.WriteLine("Agent startuje...");
                        ConsoleUi.WriteSeparator();
                        Console.WriteLine();

                        var output = new StringBuilder();
                        var toolsUsed = 0;
                        var toolCalls = new List<string>();
                        const int MAX_CONTINUATIONS = 5;

                        using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(SCENARIO_TIMEOUT_MINUTES));
                        ConsoleUi.ResetTimer();

                        try
                        {
                            for (int continuation = 0; continuation <= MAX_CONTINUATIONS; continuation++)
                            {
                                if (continuation > 0)
                                {
                                    ConsoleUi.WriteColored($"\n--- kontynuacja {continuation}/{MAX_CONTINUATIONS} ---", ConsoleColor.DarkYellow);
                                    messages.Add(new(ChatRole.User, "Kontynuuj od miejsca w ktorym skonczyles. Nie powtarzaj juz wykonanych krokow."));
                                }

                                var turnOutput = new StringBuilder();

                                await foreach (var update in chat.GetStreamingResponseAsync(messages, chatOptions, cts.Token))
                                {
                                    if (update.Text is { Length: > 0 } text)
                                    {
                                        Console.Write(text);
                                        output.Append(text);
                                        turnOutput.Append(text);
                                    }

                                    foreach (var call in update.Contents.OfType<FunctionCallContent>())
                                    {
                                        toolsUsed++;
                                        toolCalls.Add($"{call.Name} {JsonSerializer.Serialize(call.Arguments)}");
                                        ConsoleUi.WriteToolCall(toolsUsed, call.Name, call.Arguments);
                                    }

                                    foreach (var result in update.Contents.OfType<FunctionResultContent>())
                                    {
                                        ConsoleUi.WriteToolResult(result.Result?.ToString());
                                    }
                                }

                                var fullText2 = output.ToString();
                                var done = fullText2.Contains("EMP-") ||
                                        fullText2.Contains("Onboarding Complete") ||
                                        fullText2.Contains("Employee ID");

                                if (done)
                                    break;

                                if (turnOutput.Length > 0)
                                    messages.Add(new(ChatRole.Assistant, turnOutput.ToString()));
                            }
                        }
                        catch (OperationCanceledException)
                        {
                            Console.WriteLine();
                            ConsoleUi.WriteError($"Timeout! Agent przekroczyl limit {SCENARIO_TIMEOUT_MINUTES} minut.");
                        }

                        // ── Post-processing ──────────────────────────────────────────
                        Console.WriteLine();
                        Console.WriteLine();
                        ConsoleUi.WriteSeparator();

                        var fullText = output.ToString().Trim();
                        var resultsDir = ResultSaver.GetResultsDir();
                        var activeModel = useClaude ? "claude-haiku-4-5" : OLLAMA_MODEL;

                        switch (scenario)
                        {
                            case "A":
                                await ResultSaver.SaveScenarioA(fullText, toolCalls, toolsUsed, resultsDir, activeModel);
                                break;
                            case "B":
                                await ResultSaver.SaveScenarioB(fullText, toolsUsed, resultsDir, activeModel);
                                break;
                            case "C":
                                await ResultSaver.SaveScenarioC(fullText, toolsUsed, resultsDir, activeModel);
                                break;
                        }

                        Console.WriteLine();
                        Console.WriteLine($"Akcji MCP wykonanych: {toolsUsed}");
                        Console.WriteLine($"Czas: {ConsoleUi.Elapsed}");
                        ConsoleUi.WriteColored("Gotowe!", ConsoleColor.Green);

                        ConsoleUi.WaitForKey();
                    
                

PlaywrightProcessResolver, czyli klasa uruchamiająca proces Playwright MCP

                    
                        using System.Diagnostics;
                        using System.Runtime.InteropServices;

                        namespace PlaywrightMcpDemo;

                        public static class PlaywrightProcessResolver
                        {
                            public static (string Command, string[] Arguments) Resolve(bool headless)
                            {
                                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                                {
                                    return ("npx", BuildArgs(headless));
                                }

                                var candidates = new[]
                                {
                                    Path.Combine(Environment.GetFolderPath
                                    (Environment.SpecialFolder.ProgramFiles), "nodejs", "npx.cmd"),
                                    Path.Combine(Environment.GetFolderPath
                                    (Environment.SpecialFolder.ProgramFilesX86), "nodejs", "npx.cmd"),
                                    Path.Combine(Environment.GetFolderPath
                                    (Environment.SpecialFolder.ApplicationData), "nvm", "current", "npx.cmd"),
                                    Path.Combine(Environment.GetFolderPath
                                    (Environment.SpecialFolder.ApplicationData), "nvm", "v24.14.0", "npx.cmd"),
                                    Path.Combine(Environment.GetFolderPath
                                    (Environment.SpecialFolder.ApplicationData), "nvm", "v22.0.0", "npx.cmd"),
                                };

                                var npxCmd = candidates.FirstOrDefault(File.Exists);

                                if (npxCmd != null)
                                {
                                    return (npxCmd, BuildArgs(headless));
                                }

                                var fallbackArgs = new List<string> { "/c", "npx" };
                                fallbackArgs.AddRange(BuildArgs(headless));
                                return ("cmd", fallbackArgs.ToArray());
                            }

                            public static async Task<bool> VerifyAsync(string command, string[] arguments)
                            {
                                try
                                {
                                    var proc = new Process
                                    {
                                        StartInfo = new ProcessStartInfo
                                        {
                                            FileName = command,
                                            Arguments = string.Join
                                            (" ", 
                                            arguments.Select(a => 
                                            a.Contains(' ') ? $"\"{a}\"" : a)),
                                            RedirectStandardError = true,
                                            UseShellExecute = false,
                                            CreateNoWindow = true,
                                        }
                                    };

                                    proc.Start();
                                    await Task.Delay(500);

                                    if (proc.HasExited)
                                    {
                                        var stderr = await proc.StandardError.ReadToEndAsync();
                                        ConsoleUi.WriteError("Proces @playwright/mcp zakonczyl sie natychmiast!");
                                        if (proc.ExitCode != 0)
                                            ConsoleUi.WriteError($"Exit code: {proc.ExitCode}");
                                        if (!string.IsNullOrWhiteSpace(stderr))
                                            ConsoleUi.WriteError($"Stderr: {stderr}");

                                        Console.WriteLine();
                                        Console.WriteLine("Najczestsza przyczyna: brak przegladarek Playwright");
                                        Console.WriteLine("Fix: npx playwright install chromium");
                                        Console.WriteLine();
                                        return false;
                                    }

                                    proc.Kill(entireProcessTree: true);
                                    return true;
                                }
                                catch (Exception ex)
                                {
                                    ConsoleUi.WriteError($"Nie mozna uruchomic npx: {ex.Message}");
                                    return false;
                                }
                            }

                            private static string[] BuildArgs(bool headless)
                            {
                                return headless
                                    ? ["-y", "@playwright/mcp@latest", "--headless"]
                                    : ["-y", "@playwright/mcp@latest"];
                            }
                        }
                    
                

PromptLoader, czyli klasa do ładowania promptów zapisanych w plikach

                    
                        public static class PromptLoader
                        {
                            private const string PromptsDir = "prompts";

                            public static async Task<(string System, string User)> 
                                LoadAsync(string scenario, string demoPageUrl)
                            {
                                var key = scenario.ToLowerInvariant();

                                var systemPath = Path.Combine(PromptsDir, $"scenario-{key}-system.md");
                                var userPath = Path.Combine(PromptsDir, $"scenario-{key}-user.md");

                                if (!File.Exists(systemPath) || !File.Exists(userPath))
                                    throw new FileNotFoundException
                                    ($"Brak promptow dla scenariusza {scenario} w {PromptsDir}/");

                                var system = await File.ReadAllTextAsync(systemPath);
                                var user = await File.ReadAllTextAsync(userPath);

                                user = user.Replace("{{DEMO_PAGE_URL}}", demoPageUrl);

                                return (system, user);
                            }
                        }
                    
                

ResultSaver - zapis i analiza wyników scenariuszy promptów

                    
                        using System.Text;
                        using System.Text.RegularExpressions;

                        namespace PlaywrightMcpDemo;

                        public static class ResultSaver
                        {
                            public static string GetResultsDir()
                            {
                                var timestamp = DateTime.Now.ToString("yyyy-MM-dd-HHmm");
                                var dir = Path.Combine("results", timestamp);
                                Directory.CreateDirectory(dir);
                                return dir;
                            }

                            public static async Task SaveScenarioA
                                (string fullText, List<string> toolCalls, int toolsUsed, string resultsDir, string model)
                            {
                                var allCalls = string.Join(" ", toolCalls);

                                var nav = toolCalls.Any(c => c.Contains("browser_navigate"));
                                var step1 = 
                                (allCalls.Contains("Jan") 
                                || allCalls.Contains("firstName") || allCalls.Contains("e66"))
                                        && (allCalls.Contains("Kowalski") 
                                        || allCalls.Contains("lastName") || allCalls.Contains("e70"))
                                        && (allCalls.Contains("@") 
                                        || allCalls.Contains("email"));

                                var step2 = allCalls.Contains(".NET") || allCalls.Contains("position")
                                        || allCalls.Contains("e114") || allCalls.Contains("B2B")
                                        || allCalls.Contains("startDate") || allCalls.Contains("e140");

                                var step3 = allCalls.Contains("GitHub") || allCalls.Contains("ThinkPad")
                                        || allCalls.Contains("checkbox") || allCalls.Contains("Medicover")
                                        || allCalls.Contains("Datadog");

                                var step4 = allCalls.Contains("Complete") || allCalls.Contains("Submit")
                                        || fullText.Contains("Complete") || fullText.Contains("submit");

                                var empId = Regex.IsMatch(fullText + allCalls, @"EMP-\d+|NVC-\d+|employee.?id",
                                                RegexOptions.IgnoreCase);

                                Console.WriteLine();
                                Console.WriteLine("Analiza przebiegu:");
                                ConsoleUi.WriteStep("Nawigacja do strony", nav);
                                ConsoleUi.WriteStep("Krok 1 - Personal Data", step1);
                                ConsoleUi.WriteStep("Krok 2 - Employment Details", step2);
                                ConsoleUi.WriteStep("Krok 3 - System Access", step3);
                                ConsoleUi.WriteStep("Krok 4 - Submit", step4);
                                ConsoleUi.WriteStep("Employee ID odczytany", empId);

                                var steps = new[] { nav, step1, step2, step3, step4, empId };
                                var passed = steps.Count(x => x);

                                Console.WriteLine();
                                ConsoleUi.WriteColored($"Wynik: {passed}/6 krokow zaliczonych",
                                    passed == 6 ? ConsoleColor.Green :
                                    passed >= 4 ? ConsoleColor.Yellow : ConsoleColor.Red);

                                var report = new StringBuilder();
                                report.AppendLine($"# Scenariusz A : Autonomiczny Agent HR");
                                report.AppendLine($"**Data:** {DateTime.Now:yyyy-MM-dd HH:mm}");
                                report.AppendLine($"**Model:** {model}");
                                report.AppendLine($"**Akcji MCP:** {toolsUsed}");
                                report.AppendLine();
                                report.AppendLine("## Wyniki");
                                report.AppendLine($"- Nawigacja: {(nav ? "✅" : "❌")}");
                                report.AppendLine($"- Krok 1 Personal Data: {(step1 ? "✅" : "❌")}");
                                report.AppendLine($"- Krok 2 Employment: {(step2 ? "✅" : "❌")}");
                                report.AppendLine($"- Krok 3 System Access: {(step3 ? "✅" : "❌")}");
                                report.AppendLine($"- Krok 4 Submit: {(step4 ? "✅" : "❌")}");
                                report.AppendLine($"- Employee ID: {(empId ? "✅" : "❌")}");
                                report.AppendLine();
                                report.AppendLine($"**Wynik: {passed}/6**");
                                report.AppendLine();
                                report.AppendLine("## Output agenta");
                                report.AppendLine("```");
                                report.AppendLine(fullText);
                                report.AppendLine("```");
                                report.AppendLine();
                                report.AppendLine("## Tool calls");
                                for (int i = 0; i < toolCalls.Count; i++)
                                    report.AppendLine($"{i + 1}. {toolCalls[i]}");

                                var path = Path.Combine(resultsDir, "scenario-a-report.md");
                                await File.WriteAllTextAsync(path, report.ToString());
                                ConsoleUi.WriteColored($"Zapisano: {path}", ConsoleColor.Green);
                            }

                            public static async Task SaveScenarioB
                                (string fullText, int toolsUsed, string resultsDir, string model)
                            {
                                var match = Regex.Match(fullText, @"(import \{.*)", RegexOptions.Singleline);
                                var code = match.Success ? match.Value : fullText;
                                code = Regex.Replace(code, @"```\w*\n?|```", "").Trim();

                                var specPath = Path.Combine(resultsDir, "onboarding.spec.ts");
                                await File.WriteAllTextAsync(specPath, code);
                                ConsoleUi.WriteColored($"Zapisano: {specPath}", ConsoleColor.Green);
                                Console.WriteLine("   Uruchom: npx playwright test onboarding.spec.ts --headed");

                                var report = new StringBuilder();
                                report.AppendLine($"# Scenariusz B : Generator Testow E2E");
                                report.AppendLine($"**Data:** {DateTime.Now:yyyy-MM-dd HH:mm}");
                                report.AppendLine($"**Model:** {model}");
                                report.AppendLine($"**Akcji MCP:** {toolsUsed}");
                                report.AppendLine();
                                report.AppendLine("## Wygenerowany test");
                                report.AppendLine("```typescript");
                                report.AppendLine(code);
                                report.AppendLine("```");

                                var reportPath = Path.Combine(resultsDir, "scenario-b-report.md");
                                await File.WriteAllTextAsync(reportPath, report.ToString());
                                ConsoleUi.WriteColored($"Zapisano: {reportPath}", ConsoleColor.Green);
                            }

                            public static async Task SaveScenarioC
                                (string fullText, int toolsUsed, string resultsDir, string model)
                            {
                                var report = new StringBuilder();
                                report.AppendLine($"# Raport Dostepnosci WCAG 2.1 AA");
                                report.AppendLine($"**Aplikacja:** TalentFlow HR Onboarding Portal");
                                report.AppendLine($"**Data audytu:** {DateTime.Now:yyyy-MM-dd HH:mm}");
                                report.AppendLine($"**Audytor:** AI Agent ({model} via Playwright MCP)");
                                report.AppendLine($"**Standard:** WCAG 2.1 Level AA");
                                report.AppendLine();
                                report.AppendLine("---");
                                report.AppendLine();
                                report.AppendLine(fullText);

                                var path = Path.Combine(resultsDir, "accessibility-report.md");
                                await File.WriteAllTextAsync(path, report.ToString());
                                ConsoleUi.WriteColored($"Zapisano: {path}", ConsoleColor.Green);

                                Console.WriteLine();
                                Console.WriteLine("Znalezione problemy:");
                                ConsoleUi.WriteColored($"   CRITICAL : {Count(fullText, "CRITICAL")}",
                                    Count(fullText, "CRITICAL") > 0 ? ConsoleColor.Red : ConsoleColor.DarkGray);
                                ConsoleUi.WriteColored($"   MAJOR    : {Count(fullText, "MAJOR")}",
                                    Count(fullText, "MAJOR") > 0 ? ConsoleColor.DarkYellow : ConsoleColor.DarkGray);
                                ConsoleUi.WriteColored($"   MINOR    : {Count(fullText, "MINOR")}", ConsoleColor.Yellow);
                                ConsoleUi.WriteColored($"   OK       : {Count(fullText, "OK")}", ConsoleColor.Green);
                            }

                            private static int Count(string s, string p) =>
                                (s.Length - s.Replace(p, "").Length) / p.Length;
                        }
                    
                

System prompt dla scenariusza C

                    
                        Jestes certyfikowanym audytorem dostepnosci cyfrowej WCAG 2.1 AA.
                        Eksplorujesz strony przez Playwright MCP uzywajac browser_snapshot do analizy.
                        Dokumentujesz KAZDY problem z konkretna lokalizacja i poziomem WCAG.

                        Format problemow w raporcie:
                        CRITICAL - blokuje uzycie (WCAG A)
                        MAJOR    - utrudnia znaczaco (WCAG AA)
                        MINOR    - do poprawki w przyszlosci
                        OK       - zgodne z WCAG
                    
                

User prompt dla scenariusza C

                    
                        Przeprowadz pelny audyt WCAG 2.1 AA formularza: {{DEMO_PAGE_URL}}

                        Przejdz przez KAZDY krok (1 do 4) klikajac Continue i sprawdz:

                        PERCEIVABLE:
                        - Czy pola maja etykiety powiazane przez for/id lub aria-label?
                        - Czy ikony i przyciski bez tekstu maja aria-label?
                        - Czy kontrast tekstu spelnia minimum 4.5:1?
                        - Czy informacje nie sa przekazywane tylko kolorem?

                        OPERABLE:
                        - Czy wszystko jest dostepne klawiatura (Tab, Enter, Space)?
                        - Czy focus indicator jest widoczny?
                        - Czy checkboxy i radio buttons maja proper ARIA?

                        UNDERSTANDABLE:
                        - Czy wymagane pola sa wyraznie oznaczone?
                        - Czy lang jest ustawiony na elemencie html?
                        - Czy struktura naglowkow h1 do h3 jest logiczna?

                        ROBUST:
                        - Czy role ARIA sa poprawnie uzyte?
                        - Czy formularz ma poprawna semantyke HTML?

                        Wygeneruj raport Markdown z:
                        1. Podsumowaniem (liczba problemow per poziom)
                        2. Lista wszystkich problemow z rekomendacjami naprawy
                        3. Ocena koncowa w skali 0 do 100

                    
                
screenshot
To się nie skaluje
A system wieloagentowy będzie jeszcze bardziej skomplikowany
screenshot

Pisanie całego systemu agentowego od zera mija się z celem

Potrzebujemy klocków, które pozwalają budować agentów i całe procesy przepływu w systemie wieloagentowym.


Opowiem Wam, jak wyglądają one w MAF

Jakie fundamenty trzeba znać, budując system jednoagentowy i wieloagentowy?

Fundamenty
  1. Fundamenty, które pozwalają budować agentów i systemy wieloagentowe
  1. Fundamenty, które pozwalają budować agentów i systemy wieloagentowe
Pluginowość i modułowość MAF
  1. Pluginowość MAF
Pamięć i stan konwersacji w MAF

Tak, aplikacja może pamiętać kluczowe fakty o zadaniu i o Tobie, podobnie jak Claude


Jest też wsparcie dla reducerów pamięci

Jak możemy organizować współpracę agentów
Sequential
Concurrent
Handoff
path 1 path 2
Group Chat
worker reviewer
Magentic
Workflow Process
Chcemy budować system z klocków
screenshot
screenshot
Spoiler: mamy dwa mechanizmy klockowe, które budują MAF
Chcemy mieć też gotowe mechanizmy łączenia klocków w przepływy procesów
  1. Workflow w MAF
  1. Workflow w MAF
Czy mamy gotowe UI do tego wszystkiego?
Czy trzeba pisać własne UI?
screenshot
  1. Protokół AG-UI
Gotowego UI dla użytkowników jeszcze nie ma, ale jest...
Dev UI do testowania agentów
screenshot
screenshot
screenshot
screenshot
screenshot
screenshot
A dokumentacja?
Niech LLM-y nie będą Twoją dokumentacją
MAF ma świetne przykłady na GitHubie
screenshot
screenshot
screenshot
screenshot
screenshot
screenshot
screenshot
screenshot
Demo kodu, czyli co chcemy zbudować
  1. Co chcę pokazać w demo?
screenshot

Stworzyłem 4 wersje tego projektu, a Wam pokażę najlepszą

screenshot
Graf przepływu można też utworzyć w kodzie
screenshot
  1. Architektura agentowa
  1. Wzorce enterprise
Oto elementy mojego kodu
IChatClient
ujednolicona abstrakcja modelu

IChatClient

                    
                        IChatClient chatClient = new Anthropic.AnthropicClient()
                            .AsIChatClient("claude-haiku-4-5");

                        //MAF jest zbudowany 
                        //na IChatClient z Microsoft.Extensions.AI:







                        IChatClient chatClient =
                            new OpenAIClient("OPENAI_API_KEY")
                                .AsIChatClient("gpt-5");






                        IChatClient chatClient =
                            new AzureOpenAIClient(
                                new Uri("https://YOUR_RESOURCE.openai.azure.com/"),
                                new AzureKeyCredential("AZURE_KEY"))
                            .AsIChatClient("gpt-4.1");






                        IChatClient chatClient =
                            new OllamaApiClient(
                                new Uri("http://localhost:11434"),
                                "qwen3:14b")
                            .AsIChatClient();






                        IChatClient chatClient =
                            new GoogleAiClient("GOOGLE_API_KEY")
                                .AsIChatClient("gemini-2.5-pro");






                        IChatClient chatClient =
                            new MistralClient("MISTRAL_API_KEY")
                                .AsIChatClient("mistral-large");






                        IChatClient chatClient =
                            new OpenAIClient(
                                new OpenAIAuthentication("GROQ_API_KEY"),
                                new OpenAIClientSettings
                                {
                                    Endpoint = new Uri("https://api.groq.com/openai/v1")
                                })
                            .AsIChatClient("llama-3.3-70b");






                        IChatClient chatClient =
                            new OpenAIClient(
                                new OpenAIAuthentication("OPENROUTER_KEY"),
                                new OpenAIClientSettings
                                {
                                    Endpoint = new Uri("https://openrouter.ai/api/v1")
                                })
                            .AsIChatClient("anthropic/claude-sonnet-4");






                        IChatClient chatClient =
                            new OpenAIClient(
                                new OpenAIAuthentication("lm-studio"),
                                new OpenAIClientSettings
                                {
                                    Endpoint = new Uri("http://localhost:1234/v1")
                                })
                            .AsIChatClient("local-model");







                        IChatClient chatClient =
                            new OpenAIClient(
                                new OpenAIAuthentication("NVIDIA_API_KEY"),
                                new OpenAIClientSettings
                                {
                                    Endpoint = new Uri("https://integrate.api.nvidia.com/v1")
                                })
                            .AsIChatClient("meta/llama-3.1-70b-instruct");







                        IChatClient chatClient =
                            new OpenAIClient(
                                new OpenAIAuthentication("localai"),
                                new OpenAIClientSettings
                                {
                                    Endpoint = new Uri("http://localhost:8080/v1")
                                })
                            .AsIChatClient("local-model");
                                            
                

Można podmienić providera (OpenAI, Azure, Ollama...) bez zmiany kodu agentów.


Są gotowe middleware dla IChatClient (rate limiting, logging, retry), które działają transparentnie

Wyższa abstrakcja -> klasa Agent

                    
                        using Microsoft.Extensions.AI;
                        using Microsoft.Extensions.DependencyInjection;
                        using Microsoft.Agents.AI;
                        using OpenAI;

                        // ==========================================
                        // 1. Niski poziom -> IChatClient
                        // ==========================================

                        IChatClient chatClient =
                            new OpenAIClient("OPENAI_API_KEY")
                                .AsIChatClient("gpt-4.1");

                        var response = await chatClient.GetResponseAsync(
                            "Napisz krótki post o .NET 10");

                        Console.WriteLine(response.Text);

                        //prompt -> model -> response

                        // ==========================================
                        // 2. Wyższa abstrakcja -> Agent
                        // ==========================================

                        ChatClientAgent linkedinAgent = new(
                            chatClient,
                            new ChatClientAgentOptions
                            {
                                Name = "LinkedInWriter",

                                Instructions = """
                                    Jesteś seniorem copywriterem LinkedIn.

                                    Zasady:
                                    - pisz technicznie
                                    - używaj krótkich akapitów
                                    - max 2 emoji
                                    - dodaj CTA
                                    - odpowiadaj po polsku
                                    """
                            });

                        var agentResponse = await linkedinAgent.RunAsync(
                            "Napisz post o .NET 10");

                        await foreach (var message in agentResponse)
                        {
                            Console.Write(message.Text);
                        }

                        //user
                        //↓
                        //Agent
                        //↓
                        //Instructions
                        //Tools
                        //Memory
                        //Context
                        //Routing
                        //Policies
                        //↓
                        //IChatClient
                        //↓
                        //LLM

                                            
                
Wyzwanie z Human-in-the-Loop (HITL)
  1. Human-in-the-Loop (HITL) na dwa sposoby

RequestPort jest executorem w grafie workflow.

Workflow nie może go ominąć

Jest częścią grafu, nie opcjonalnym narzędziem agenta.

Jak wygląda zatwierdzenie w kodzie

                    
                        // W event loop klienta:
                        if (evt is RequestInfoEvent ri)
                        {
                            // Wyświetl podsumowanie użytkownikowi
                            // Pobierz decyzję (t/n + feedback)
                            var decision = new ApprovalDecision(approved, feedback);
                            await run.SendResponseAsync(
                                evt.Request.CreateResponse(decision));
                        }
                    
                
Wywołanie Skills (AgentSkillsProvider)

Skills to paczki wiedzy domenowej w formacie SKILL.md

Markdown z instrukcjami, które agent ładuje na żądanie (progressive disclosure).

Nie upychamy wszystkiego w system prompt.

  1. Trzy fazy ładowania:

Przykładowy kod, który dodaje skills

                    
                        var skillsProvider = new AgentSkillsProvider(
                            Path.Combine(AppContext.BaseDirectory, "skills"));

                        // Podpięcie do agenta jako context provider:
                        _agent = new ChatClientAgent
                            (chatClient, new ChatClientAgentOptions
                        {
                            Name = "Copywriter",
                            AIContextProviders = [skillsProvider],
                            ChatOptions = new() { Instructions = "..." }
                        });
                    
                

AgentSkillsProvider skanuje katalog skills/, odkrywa pliki SKILL.md i rejestruje je

  1. Trzy sposoby definiowania skills (.NET)
Wywołanie prawdziwego serwera MCP
screenshot

Wywołanie prawdziwego serwera MCP

                    
                        using ModelContextProtocol.Server;
                        using System.ComponentModel;

                        var builder = WebApplication.CreateBuilder(args);

                        builder.Services.AddMcpServer()
                            .WithHttpTransport()
                            .WithTools<YouTubeTranscriptTool>();

                        var app = builder.Build();

                        app.MapMcp("/mcp");

                        app.Run("http://localhost:2001");


                        [McpServerToolType]
                        public class YouTubeTranscriptTool
                        {
                            [McpServerTool(Name = "get_youtube_transcript")]
                            [Description("Pobiera transkrypcję wideo z YouTube.")]
                            public static string Execute(
                                [Description("URL wideo YouTube")] string videoUrl)
                            {
                                Console.WriteLine($"[MCP Server] Pobieram: {videoUrl}");

                                return """
                                    Tytuł: "Microsoft Agent Framework 1.0 : Production Ready!"
                                    Kanał: dotNET (Microsoft), Data: 7 maja 2026

                                    Kluczowe punkty:
                                    - MAF 1.0 = fuzja Semantic Kernel + AutoGen → stabilne API, SLA produkcyjne
                                    - Workflows: sequential, concurrent, handoff : wszystko ze streamingiem
                                    - MCP jako pierwszoklasowy obywatel: dynamiczne odkrywanie tools
                                    - Human-in-the-Loop: ApprovalRequiredAIFunction : jeden wrapper
                                    - Agent Skills: wiedza dziedzinowa w SKILL.md, progressive disclosure
                                    """;
                            }
                        }
                    
                
Dodanie MCP do agenta wygląda tak

Wywołanie prawdziwego serwera MCP

                    
                        // Połączenie z serwerem MCP (HTTP transport):
                        await using var mcpClient = await McpClient.CreateAsync(
                            new HttpClientTransport(new HttpClientTransportOptions
                            {
                                Endpoint = new Uri("http://localhost:2001/mcp")
                            }));

                        // Pobranie listy narzędzi:
                        IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();

                        // Przekazanie narzędzi do agenta:
                        _agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
                        {
                            ChatOptions = new()
                            {
                                Instructions = "...",
                                Tools = [.. mcpTools]  // spread operator
                            }
                        });
                    
                
Wywołanie metody C# jako narzędzia agenta

ChatOptions w agencie

                    
                        ChatOptions = new()
                        {
                            Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
                        }

                        [Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
                        static string GetLinkedInTrends(
                            [Description("Kategoria, np. 'AI', '.NET'")] string category)
                        {
                            return $"Top trendy w kategorii \"{category}\" (symulacja): ...";
                        }

                        //Kluczowa różnica vs Semantic Kernel

                        //W SK trzeba było: stworzyć plugin 
                        //    → zarejestrować go w Kernel 
                        //    → oznaczyć [KernelFunction]. 

                        //W MAF wystarczy zwykła metoda C# + AIFunctionFactory.Create() 
                        //    : zero boilerplate'u, zero zależności od obiektu Kernel.
                    
                
Executor jako warstwa abstrakcji
Executor to fundamentalny klocek LEGO Duplo w MAF.
screenshot
  1. Co robi klocek LEGO Executor:
  1. Co robi klocek LEGO Executor 2:

Rodzaje executorów w kodzie:

Executor Typ Co robi
ResearcherExecutor AI Agent + narzędzia MCP Pobiera transkrypcję YT, wyodrębnia tezy
CopywriterExecutor AI Agent + Skills Pisze draft posta na LinkedIn
StrategExecutor AI Agent + Skills + C# function tool Analizuje trendy, proponuje strategię
AggregatorExecutor Deterministyczny Zbiera wyniki fan-in (draft + strategia)
RedaktorExecutor AI Agent Scala draft i strategię w finalny post
PublishExecutor Deterministyczny Symuluje publikację, robi YieldOutput
PrepareApprovalExecutor<T> Deterministyczny Przygotowuje podsumowanie do zatwierdzenia
ApprovalGateExecutor<T> Deterministyczny Sprawdza decyzję, audit log, przepuszcza/odrzuca

Executor

                    
                        // Executor z typowanym wejściem i wyjściem:
                        public partial class RedaktorExecutor : 
                            Executor<EditorialInput, FinalPost>

                        // Executor z wejściem bez typowanego wyjścia (wysyła ręcznie):
                        public partial class AggregatorExecutor() : 
                            Executor<object>("Aggregator")

                        // Executor z wejściem i YieldOutput:
                        public partial class PublishExecutor() : 
                            Executor<FinalPost>("Publish")
                    
                

Executor zapewnia separację: każdy krok jest niezależny, testowalny, może być AI lub zwykłym kodem.

Executor NIE jest agentem AI.

Executor to warstwa orkiestracyjna, która używa agenta. Agent jest wstrzykiwany z zewnątrz

ResearcherExecutor

                    
                        // Executor przyjmuje gotowego AIAgent : nie tworzy go sam
                        public ResearcherExecutor(AIAgent agent) : base("Researcher")
                        {
                            _agent = agent;
                        }
                    
                

Workflow defininiowany jako graf executorów z krawędziami definiującymi przepływ danych i zależności

WorkflowBuilder

                    
                        var workflow = new WorkflowBuilder(researcher)
                            .AddEdge(researcher, research.Prepare)
                            .AddEdge(research.Prepare, research.Port)
                            // ...
                            .AddFanOutEdge(research.Gate, [copywriter, strateg])
                            .AddFanInBarrierEdge(fanInSources, aggregator)
                            // ...
                            .Build();
                    
                
screenshot
screenshot
Workflow to także wbudowana architekrua event streaming

WorkflowEvent łapanie

                    
                        await foreach (WorkflowEvent evt in run.WatchStreamAsync())
                        {
                            EventAction action = evt switch
                            {
                                ResearchCompletedEvent e => 
                                    renderer.HandleResearchCompleted(e),
                                RequestInfoEvent ri     => 
                                    await renderer.HandleHITL(ri, run),
                                ExecutorFailedEvent e   => 
                                    renderer.HandleError(e),
                                WorkflowOutputEvent e   => 
                                    renderer.HandleOutput(e),
                                _ => EventAction.Next
                            };
                        }
                    
                
  1. Framework events (wbudowane w MAF):
  1. Framework events (wbudowane w MAF):

Wysłanie eventu

                    
                        public sealed class ResearchCompletedEvent
                            (ResearchResult result) : WorkflowEvent(result)
                        {
                            public ResearchResult Result => result;
                        }

                        await context.AddEventAsync
                            (new ResearchCompletedEvent(result), cancellationToken);
                    
                
  1. Custom events
Type-safe message routing

Każdy executor deklaruje typy wejściowe i wyjściowe za pomocą generyków i atrybutów:

Program.cs : przykładowy kod

                    
                        [YieldsOutput(typeof(ResearchResult))]   
                        // "ten executor produkuje ResearchResult"
                        public partial class ResearcherExecutor ...





                        [YieldsOutput(typeof(DraftPost))]        
                        // "ten executor produkuje DraftPost"
                        public partial class CopywriterExecutor ...



                        
                        [YieldsOutput(typeof(FinalPost))]        
                        // "ten executor produkuje FinalPost"
                        public partial class RedaktorExecutor ...




                        [SendsMessage(typeof(EditorialInput))]   
                        // "ten executor wysyła EditorialInput dalej"
                        public partial class AggregatorExecutor ...




                        [SendsMessage(typeof(object))]           
                        // "ten executor wysyła dowolny typ dalej"
                        public partial class ApprovalGateExecutor<TData> ...

                    
                
  1. Na podstawie tych atrybutów generator w compile-time tworzy:
  1. Co to daje:
  1. Co to daje:
  1. Co to daje:

Jak to wygląda w praktyce: mapa typów w grafie:

                    
                        ResearchRequest
                            → Researcher → ResearchResult
                                → PrepareResearch → ApprovalRequest
                                    → RequestPort → ApprovalDecision
                                        → GateResearch → ResearchResult (przepuszczone)
                                            ├→ Copywriter → DraftPost
                                            └→ Strateg → StrategyAnalysis
                                                → Aggregator → EditorialInput
                                                    → PrepareEditorial → ApprovalRequest
                                                        → RequestPort → ApprovalDecision
                                                            → GateEditorial → EditorialInput (przepuszczone)
                                                                → Redaktor → FinalPost
                                                                    → PreparePublish → ApprovalRequest
                                                                        → RequestPort → ApprovalDecision
                                                                            → GatePublish → FinalPost 
                                                                                (przepuszczone)
                                                                                → Publish → string (output)
                    
                

MAF automatycznie generuje kod połączeń między elementami workflow już podczas kompilacji.

To mechanizm, w którym kompilator analizuje atrybuty na executorach i automatycznie tworzy dodatkowy kod C#, który zapewnia type-safety przepływu danych w grafie workflow.

Każde połączenie między elementami jest sprawdzane przez kompilator. Żadna wiadomość nie "zgubi się" ani nie trafi do niewłaściwego executora.


Analogia: To jak generyczne interfejsy w ASP.NET Core (IRequestHandler<TRequest, TResponse> z MediatR), ale zamiast ręcznej rejestracji w DI, source generator robi to automatycznie na podstawie atrybutów i hierarchii klas.

  1. Podsumowanie: type-safe message routing
Middleware
dlaczego są fajne
screenshot
Middleware
to nasze mniejsze klocki

MAF pozwala wpiąć middleware na 3 różnych poziomach, każdy z inną sygnaturą, innym punktem interwencji i innym celem.

To kluczowa różnica vs Semantic Kernel, który miał płaski model filtrów.

Poziomy 1,2, 3

                    
                        ┌─────────────────────────────────────────────────────────────────────┐
                        │ Poziom 1: Agent Run Middleware                                      │
                        │   widzi: cały prompt → cała odpowiedź                               │
                        │   przykłady: Logging, Retry, Guardrails, TokenTracking              │
                        │                                                                     │
                        │   ┌─────────────────────────────────────────────────────────────┐   │
                        │   │ Poziom 2: Function Calling Middleware                       │   │
                        │   │   widzi: nazwa narzędzia, argumenty → wynik narzędzia       │   │
                        │   │   przykłady: AuditFunction, InputValidation                 │   │
                        │   │                                                             │   │
                        │   │   ┌─────────────────────────────────────────────────────┐   │   │
                        │   │   │ Poziom 3: IChatClient Middleware                    │   │   │
                        │   │   │   widzi: surowe tokeny → surowa odpowiedź LLM       │   │   │
                        │   │   │   przykłady: RateLimiter, CachingClient,            │   │   │
                        │   │   │              TelemetryClient, TokenCounter          │   │   │
                        │   │   │                                                     │   │   │
                        │   │   │        ┌──────────────────────────────────┐         │   │   │
                        │   │   │        │  Model LLM (Anthropic, OpenAI,   │         │   │   │
                        │   │   │        │  Ollama...)                      │         │   │   │
                        │   │   │        └──────────────────────────────────┘         │   │   │
                        │   │   └─────────────────────────────────────────────────────┘   │   │
                        │   └─────────────────────────────────────────────────────────────┘   │
                        └─────────────────────────────────────────────────────────────────────┘
                    
                
  1. Przykładowe middleware, które napisałem
  1. Middleware
  1. Middleware
  1. Middleware
Guardrails
dlaczego są fajne
  1. W kodzie napisałem 3 guardrails:

GuardrailAction

                    
                        public enum GuardrailAction { Block, Modify, Warn }

                        Block : output zablokowany, zwrócony komunikat błędu
                        Modify : output zmodyfikowany (np. zredagowane PII)
                        Warn : ostrzeżenie w logach, output przepuszczony
                    
                
  1. Dlaczego guardrails w kodzie są lepsze niż w prompcie:
  1. Dlaczego guardrails w kodzie są lepsze niż w prompcie:
Oto cały kod projektu
screenshot

Program.cs : HITL Pipeline + Middleware + Guardrails

                    
                        // ============================================================================
                        // Program.cs tworzy agentów, podpina middleware/guardrails, przekazuje do executorów.
                        //
                        // Pipeline middleware per agent (od zewnątrz do wewnątrz):
                        //   Logging → Retry → Guardrails → FunctionAudit → FunctionValidation → Agent
                        //
                        // Konfiguracja per agent:
                        //   Researcher  : Logging + Retry + FunctionAudit + FunctionValidation (bez guardrails)
                        //   Copywriter  : Logging + Retry + ContentPolicy + BrandSafety + OutputLength
                        //   Strateg     : Logging + Retry + OutputLength
                        //   Redaktor    : Logging + Retry + ContentPolicy + BrandSafety + OutputLength
                        //
                        // ============================================================================

                        using Microsoft.Agents.AI;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;
                        using ModelContextProtocol.Client;
                        using AiContentTeam;
                        using AiContentTeam.Middleware;
                        using AiContentTeam.Guardrails;

                        // ── Chat client (surowy, bez middleware na IChatClient) ────────────────────────

                        IChatClient chatClient = new Anthropic.AnthropicClient()
                            .AsIChatClient("claude-haiku-4-5");

                        // ── MCP tools ────────────────────────────────────────────────────────────────

                        await using var mcpClient = await McpClient.CreateAsync(
                            new HttpClientTransport(new HttpClientTransportOptions
                            {
                                Endpoint = new Uri("http://localhost:2001/mcp")
                            }));

                        IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();

                        // ── Skills provider ───────────────────────────────────────────────────────────

                        #pragma warning disable MAAI001
                        var skillsProvider = new AgentSkillsProvider(
                            Path.Combine(AppContext.BaseDirectory, "skills"));
                        #pragma warning restore MAAI001

                        // ── Guardrails (reużywalne instancje) ─────────────────────────────────────────

                        var contentPolicy = new ContentPolicyGuardrail();
                        var brandSafety = new BrandSafetyGuardrail(
                            competitors: ["CompetitorA", "CompetitorB"],
                            requireCTA: true);
                        var outputLength = new OutputLengthGuardrail(maxChars: 3000);

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  TWORZENIE AGENTÓW Z MIDDLEWARE
                        // ══════════════════════════════════════════════════════════════════════════════

                        // ── Researcher: Logging + Retry + FunctionAudit + FunctionValidation ──────────
                        //    Bez guardrails : pracuje na surowych danych z zewnątrz (transkrypcja YouTube).

                        AIAgent researcherAgent = GuardrailFactory.WrapWithFullPipeline(
                            new ChatClientAgent(
                                chatClient,
                                new ChatClientAgentOptions
                                {
                                    Name = "Researcher",
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś research agentem. Twoim JEDYNYM zadaniem jest:
                                            1. UŻYJ NARZĘDZIA GetYouTubeTranscript aby pobrać transkrypcję z podanego URL
                                            2. Na podstawie wyniku wyodrębnij dokładnie 5 kluczowych tez jako bullet pointy

                                            ZASADY:
                                            - ZAWSZE wywołuj narzędzie GetYouTubeTranscript : NIGDY nie oceniaj URL samodzielnie
                                            - Odpowiadaj po polsku
                                            - Każdą tezę pisz w osobnej linii zaczynającej się od "• "
                                            """,
                                        Tools = [.. mcpTools]
                                    }
                                }),
                            enableLogging: true,
                            enableRetry: true);
                        // Brak guardrails : celowo

                        // ── Copywriter: pełny pipeline z guardrails ───────────────────────────────────

                        AIAgent copywriterAgent = GuardrailFactory.WrapWithFullPipeline(
                            new ChatClientAgent(
                                chatClient,
                                new ChatClientAgentOptions
                                {
                                    Name = "Copywriter",
                                    AIContextProviders = [skillsProvider],
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś copywriterem specjalizującym się w treściach dla .NET developerów.
                                            Na podstawie tez od Researchera napisz post na LinkedIn.

                                            ZASADY:
                                            - 150-250 słów, max 3 emoji
                                            - Mocny hook w 1. zdaniu (zaskoczenie lub kontrowersyjny claim)
                                            - Zakończ pytaniem do społeczności
                                            - Napisz TYLKO treść posta, bez meta-komentarza
                                            - Odpowiadaj po polsku
                                            """
                                    }
                                }),
                            enableLogging: true,
                            enableRetry: true,
                            contentPolicy, brandSafety, outputLength);

                        // ── Strateg: Logging + Retry + OutputLength ───────────────────────────────────

                        AIAgent strategAgent = GuardrailFactory.WrapWithFullPipeline(
                            new ChatClientAgent(
                                chatClient,
                                new ChatClientAgentOptions
                                {
                                    Name = "Strateg",
                                    AIContextProviders = [skillsProvider],
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś strategiem social media dla społeczności .NET developerów.
                                            Przeanalizuj temat i odpowiedz STRUKTURALNIE:

                                            ANALIZA TRENDÓW: [2-3 zdania o aktualności tematu]
                                            REKOMENDOWANY HOOK: [jedno zdanie otwierające : konkretna propozycja]
                                            SUGEROWANE HASHTAGI: [lista, max 5, format #tag]
                                            UWAGI O ZAANGAŻOWANIU: [1-2 zdania]

                                            Odpowiadaj po polsku.
                                            """,
                                        Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
                                    }
                                }),
                            enableLogging: true,
                            enableRetry: true,
                            outputLength);

                        // ── Redaktor: pełny pipeline z guardrails ─────────────────────────────────────

                        AIAgent redaktorAgent = GuardrailFactory.WrapWithFullPipeline(
                            new ChatClientAgent(
                                chatClient,
                                new ChatClientAgentOptions
                                {
                                    Name = "Redaktor",
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś redaktorem naczelnym. Dostałeś:
                                            1. Draft posta od Copywritera
                                            2. Analizę strategiczną od Stratega

                                            Weź draft Copywritera jako bazę, zastosuj rekomendacje Stratega
                                            (hook, hashtagi, uwagi o zaangażowaniu), popraw błędy językowe.

                                            Napisz TYLKO finalną treść posta. Bez meta-komentarzy,
                                            bez opisywania co robisz. Sam tekst posta po polsku.
                                            """
                                    }
                                }),
                            enableLogging: true,
                            enableRetry: true,
                            contentPolicy, brandSafety, outputLength);

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  EXECUTORY (przyjmują gotowych agentów)
                        // ══════════════════════════════════════════════════════════════════════════════

                        var researcher = new ResearcherExecutor(researcherAgent);
                        var copywriter = new CopywriterExecutor(copywriterAgent);
                        var strateg = new StrategExecutor(strategAgent);
                        var aggregator = new AggregatorExecutor();
                        var redaktor = new RedaktorExecutor(redaktorAgent);
                        var publisher = new PublishExecutor();

                        // ── HITL stages ───────────────────────────────────────────────────────────────

                        var research = ApprovalStage.Create<ResearchResult>(
                            "Research",
                            r => $"""
                                URL: {r.YouTubeUrl}
                                Grupa docelowa: {r.TargetAudience}
                                Tezy ({r.KeyPoints.Length}):
                                {string.Join("\n", r.KeyPoints.Select(p => $"  • {p}"))}
                                """,
                            timeout: TimeSpan.FromMinutes(5));

                        var editorial = ApprovalStage.Create<EditorialInput>(
                            "Editorial",
                            ei => $"""
                                DRAFT ({ei.Draft.Content.Length} znaków):
                                {ei.Draft.Content}

                                STRATEGIA:
                                Hook: {ei.Strategy.RecommendedHook}
                                Hashtagi: {string.Join(" ", ei.Strategy.SuggestedHashtags)}
                                Uwagi: {ei.Strategy.EngagementNotes}
                                """,
                            timeout: TimeSpan.FromMinutes(5));

                        var publish = ApprovalStage.Create<FinalPost>(
                            "Publish",
                            fp => $"""
                                FINALNY POST (zredagowany):
                                {fp.Content}

                                Hashtagi: {string.Join(" ", fp.Hashtags)}
                                Zredagowany o: {fp.EditedAt}
                                """,
                            timeout: TimeSpan.FromMinutes(5));

                        // ── Workflow ──────────────────────────────────────────────────────────────────

                        IEnumerable<ExecutorBinding> fanInSources = [copywriter, strateg];

                        var workflow = new WorkflowBuilder(researcher)
                            .AddEdge(researcher, research.Prepare)
                            .AddEdge(research.Prepare, research.Port)
                            .AddEdge(research.Port, research.Gate)
                            .AddFanOutEdge(research.Gate, [copywriter, strateg])
                            .AddFanInBarrierEdge(fanInSources, aggregator)
                            .AddEdge(aggregator, editorial.Prepare)
                            .AddEdge(editorial.Prepare, editorial.Port)
                            .AddEdge(editorial.Port, editorial.Gate)
                            .AddEdge(editorial.Gate, redaktor)
                            .AddEdge(redaktor, publish.Prepare)
                            .AddEdge(publish.Prepare, publish.Port)
                            .AddEdge(publish.Port, publish.Gate)
                            .AddEdge(publish.Gate, publisher)
                            .WithOutputFrom(publisher)
                            .Build();

                        await CreateDiagrams(workflow);
                        Console.ReadKey();

                        // ── Uruchomienie ──────────────────────────────────────────────────────────────

                        ConsoleWorkflowRenderer.PrintBanner();

                        var input = new ResearchRequest(
                            YouTubeUrl: "https://youtube.com/watch?v=maf100demo",
                            TargetAudience: "polscy .NET developerzy, seniorzy i architekci");

                        var renderer = new ConsoleWorkflowRenderer(
                            finalAgentName: "Publish",
                            parallelAgents: ["Copywriter", "Strateg"]);

                        await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input);
                        await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

                        await foreach (WorkflowEvent evt in run.WatchStreamAsync())
                        {
                            EventAction action = evt switch
                            {
                                ResearchCompletedEvent e => renderer.HandleResearchCompleted(e),
                                DraftReadyEvent e => renderer.HandleDraftReady(e),
                                StrategyReadyEvent e => renderer.HandleStrategyReady(e),
                                AgentProgressEvent e => renderer.HandleAgentProgress(e),
                                ApprovalCompletedEvent e => renderer.HandleApprovalCompleted(e),
                                ExecutorFailedEvent e => renderer.HandleError(e),
                                AgentResponseUpdateEvent e => renderer.HandleToken(e),
                                WorkflowOutputEvent e => renderer.HandleOutput(e),
                                ExecutorCompletedEvent e => renderer.HandleCompleted(e),
                                _ => EventAction.Next
                            };

                            if (evt is RequestInfoEvent ri)
                            {
                                action = await renderer.HandleHITL(ri, run);
                            }

                            switch (action)
                            {
                                case EventAction.Break: goto done;
                                case EventAction.Continue: continue;
                            }
                        }
                        done:

                        renderer.PrintSummary();
                        MiddlewareMetrics.PrintReport();

                        Console.ReadLine();

                        // ── Helper: GetLinkedInTrends (tool Stratega) ─────────────────────────────────

                        [System.ComponentModel.Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
                        static string GetLinkedInTrends(
                            [System.ComponentModel.Description("Kategoria, np. 'AI', '.NET'")] string category)
                        {
                            return $"""
                                Top trendy w kategorii "{category}" (symulacja):
                                1. Microsoft Agent Framework : wysoka aktywność
                                2. .NET 10 Preview : umiarkowane zaangażowanie
                                3. Multi-agent orchestration : rosnący trend
                                """;
                        }

                        // ── Diagram export ────────────────────────────────────────────────────────────

                        static async Task CreateDiagrams(Workflow workflow)
                        {
                            var vizDir = Path.Combine(AppContext.BaseDirectory, "workflow_viz");
                            Directory.CreateDirectory(vizDir);

                            var mermaidPath = Path.Combine(vizDir, "workflow_graph.mmd");
                            await File.WriteAllTextAsync(mermaidPath, workflow.ToMermaidString());
                            Console.WriteLine($"[VIZ] Mermaid zapisany:  {mermaidPath}");

                            var dotPath = Path.Combine(vizDir, "workflow_graph.dot");
                            await File.WriteAllTextAsync(dotPath, workflow.ToDotString());
                            Console.WriteLine($"[VIZ] DOT zapisany:      {dotPath}");

                            Console.WriteLine($"[VIZ] Wszystkie pliki w: {vizDir}");
                            Console.WriteLine();
                        }

                        public enum EventAction
                        {
                            Next,
                            Continue,
                            Break
                        }
                    
                

AgentMiddleware.cs : Middleware Pipeline

                    
                        // ============================================================================
                        //   1. Agent Run Middleware
                        //      - LoggingMiddleware           : logowanie wejścia/wyjścia + timing
                        //      - RetryMiddleware             : retry z exponential backoff
                        //
                        //   2. Function Calling Middleware
                        //      - AuditFunctionMiddleware     : audyt każdego wywołania narzędzia
                        //      - InputValidationMiddleware   : walidacja/obcinanie wyników narzędzi
                        //
                        //   3. IChatClient Middleware
                        //      - TokenTrackingMiddleware     : zlicza tokeny i koszty
                        //
                        // ============================================================================

                        using Microsoft.Agents.AI;
                        using Microsoft.Extensions.AI;
                        using System.Collections.Concurrent;
                        using System.Diagnostics;
                        using System.Runtime.CompilerServices;

                        namespace AiContentTeam.Middleware;

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  1. AGENT RUN MIDDLEWARE
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Centralne logowanie i pomiar czasu każdego uruchomienia agenta.
                        /// </summary>
                        public static class LoggingMiddleware
                        {
                            public static async Task<AgentResponse> RunAsync(
                                IEnumerable<ChatMessage> messages,
                                AgentSession? session,
                                AgentRunOptions? options,
                                AIAgent innerAgent,
                                CancellationToken cancellationToken)
                            {
                                var agentName = innerAgent.Name ?? "Unknown";
                                var messageCount = messages.Count();
                                var sw = Stopwatch.StartNew();

                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"  [MW:Log] ▶ {agentName} start ({messageCount} wiadomości)");
                                Console.ResetColor();

                                try
                                {
                                    var response = await innerAgent.RunAsync(
                                        messages, session, options, cancellationToken).ConfigureAwait(false);

                                    sw.Stop();
                                    var outputLength = response.Text?.Length ?? 0;

                                    Console.ForegroundColor = ConsoleColor.DarkGray;
                                    Console.WriteLine($"  [MW:Log] ◀ {agentName} zakończony " +
                                        $"({sw.ElapsedMilliseconds}ms, {outputLength} znaków output)");
                                    Console.ResetColor();

                                    MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: true);
                                    return response;
                                }
                                catch (Exception ex)
                                {
                                    sw.Stop();
                                    Console.ForegroundColor = ConsoleColor.Red;
                                    Console.WriteLine($"  [MW:Log] ✗ {agentName} BŁĄD po {sw.ElapsedMilliseconds}ms: {ex.Message}");
                                    Console.ResetColor();

                                    MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: false);
                                    throw;
                                }
                            }

                            public static async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
                                IEnumerable<ChatMessage> messages,
                                AgentSession? session,
                                AgentRunOptions? options,
                                AIAgent innerAgent,
                                [EnumeratorCancellation] CancellationToken cancellationToken)
                            {
                                var agentName = innerAgent.Name ?? "Unknown";
                                var sw = Stopwatch.StartNew();

                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"  [MW:Log] ▶ {agentName} streaming start");
                                Console.ResetColor();

                                await foreach (var update in innerAgent.RunStreamingAsync(
                                    messages, session, options, cancellationToken))
                                {
                                    yield return update;
                                }

                                sw.Stop();
                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"  [MW:Log] ◀ {agentName} streaming zakończony ({sw.ElapsedMilliseconds}ms)");
                                Console.ResetColor();

                                MiddlewareMetrics.RecordAgentRun(agentName, sw.Elapsed, success: true);
                            }
                        }

                        /// <summary>
                        /// Retry z exponential backoff na transient failures.
                        /// </summary>
                        public static class RetryMiddleware
                        {
                            public static int MaxRetries { get; set; } = 3;
                            public static TimeSpan InitialDelay { get; set; } = TimeSpan.FromSeconds(1);

                            public static async Task<AgentResponse> RunAsync(
                                IEnumerable<ChatMessage> messages,
                                AgentSession? session,
                                AgentRunOptions? options,
                                AIAgent innerAgent,
                                CancellationToken cancellationToken)
                            {
                                var agentName = innerAgent.Name ?? "Unknown";
                                Exception? lastException = null;

                                for (int attempt = 0; attempt <= MaxRetries; attempt++)
                                {
                                    try
                                    {
                                        if (attempt > 0)
                                        {
                                            var delay = InitialDelay * Math.Pow(2, attempt - 1);
                                            Console.ForegroundColor = ConsoleColor.Yellow;
                                            Console.WriteLine($"  [MW:Retry] {agentName} próba {attempt + 1}/{MaxRetries + 1} " +
                                                $"(po {delay.TotalSeconds:F1}s)");
                                            Console.ResetColor();
                                            await Task.Delay(delay, cancellationToken);
                                        }

                                        return await innerAgent.RunAsync(
                                            messages, session, options, cancellationToken).ConfigureAwait(false);
                                    }
                                    catch (Exception ex) when (IsTransient(ex) && attempt < MaxRetries)
                                    {
                                        lastException = ex;
                                        Console.ForegroundColor = ConsoleColor.Yellow;
                                        Console.WriteLine($"  [MW:Retry] {agentName} transient failure: {ex.Message}");
                                        Console.ResetColor();
                                    }
                                }

                                throw new InvalidOperationException(
                                    $"Agent '{agentName}' failed after {MaxRetries + 1} attempts.", lastException);
                            }

                            private static bool IsTransient(Exception ex)
                            {
                                if (ex is HttpRequestException or TimeoutException
                                    or TaskCanceledException { InnerException: TimeoutException })
                                {
                                    return true;
                                }

                                if (ex is InvalidOperationException ioe
                                    && ioe.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase))
                                {
                                    return true;
                                }

                                return false;
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  2. FUNCTION CALLING MIDDLEWARE
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Audytuje każde wywołanie narzędzia : kto, co, kiedy, ile trwało.
                        /// </summary>
                        public static class AuditFunctionMiddleware
                        {
                            public static async ValueTask<object?> InvokeAsync(
                                AIAgent agent,
                                FunctionInvocationContext context,
                                Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
                                CancellationToken cancellationToken)
                            {
                                var functionName = context.Function.Name;
                                var agentName = agent.Name ?? "Unknown";
                                var sw = Stopwatch.StartNew();

                                Console.ForegroundColor = ConsoleColor.DarkCyan;
                                Console.WriteLine($"  [MW:Audit] 🔧 {agentName} → {functionName}()");
                                Console.ResetColor();

                                var result = await next(context, cancellationToken);

                                sw.Stop();
                                Console.ForegroundColor = ConsoleColor.DarkCyan;
                                Console.WriteLine($"  [MW:Audit] ✓ {functionName} zakończony ({sw.ElapsedMilliseconds}ms)");
                                Console.ResetColor();

                                MiddlewareMetrics.RecordFunctionCall(agentName, functionName, sw.Elapsed);
                                return result;
                            }
                        }

                        /// <summary>
                        /// Obcina nadmiernie długie wyniki narzędzi : zapobiega przepełnieniu kontekstu.
                        /// </summary>
                        public static class InputValidationMiddleware
                        {
                            public static int MaxResultLength { get; set; } = 10_000;

                            public static async ValueTask<object?> InvokeAsync(
                                AIAgent agent,
                                FunctionInvocationContext context,
                                Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
                                CancellationToken cancellationToken)
                            {
                                var functionName = context.Function.Name;
                                var result = await next(context, cancellationToken);

                                var resultStr = result?.ToString() ?? "";
                                if (resultStr.Length > MaxResultLength)
                                {
                                    Console.ForegroundColor = ConsoleColor.Yellow;
                                    Console.WriteLine($"  [MW:Validation] ⚠ {functionName} wynik obcięty " +
                                        $"({resultStr.Length} → {MaxResultLength} znaków)");
                                    Console.ResetColor();

                                    return resultStr[..MaxResultLength] + "... [TRUNCATED BY MIDDLEWARE]";
                                }

                                return result;
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  3. IChatClient MIDDLEWARE
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Śledzenie zużycia tokenów i szacowanie kosztów.
                        /// Rejestrowane WEWNĄTRZ agenta (przez clientFactory w konstruktorze executora).
                        /// </summary>
                        public static class TokenTrackingMiddleware
                        {
                            public static async Task<AgentResponse> RunAsync(
                                IEnumerable<ChatMessage> messages,
                                AgentSession? session,
                                AgentRunOptions? options,
                                AIAgent innerAgent,
                                CancellationToken cancellationToken)
                            {
                                var response = await innerAgent.RunAsync(
                                    messages, session, options, cancellationToken).ConfigureAwait(false);

                                var inputChars = messages.Sum(m => m.Text?.Length ?? 0);
                                var outputChars = response.Text?.Length ?? 0;

                                // Przybliżone szacowanie (1 token ≈ 4 znaki)
                                MiddlewareMetrics.RecordTokenUsage(inputChars / 4, outputChars / 4);

                                return response;
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  MIDDLEWARE METRICS
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Thread-safe centralny zbieracz metryk z pipeline'u middleware.
                        /// </summary>
                        public static class MiddlewareMetrics
                        {
                            private static readonly ConcurrentBag<AgentRunMetric> _agentRuns = [];
                            private static readonly ConcurrentBag<FunctionCallMetric> _functionCalls = [];
                            private static readonly ConcurrentBag<string> _guardrailBlocks = [];
                            private static long _totalInputTokens;
                            private static long _totalOutputTokens;

                            public record AgentRunMetric(string AgentName, TimeSpan Duration, bool Success, DateTime Timestamp);
                            public record FunctionCallMetric(string AgentName, string FunctionName, TimeSpan Duration, DateTime Timestamp);

                            public static void RecordAgentRun(string agentName, TimeSpan duration, bool success)
                                => _agentRuns.Add(new(agentName, duration, success, DateTime.UtcNow));

                            public static void RecordFunctionCall(string agentName, string functionName, TimeSpan duration)
                                => _functionCalls.Add(new(agentName, functionName, duration, DateTime.UtcNow));

                            public static void RecordGuardrailBlock(string reason)
                                => _guardrailBlocks.Add(reason);

                            public static void RecordTokenUsage(int inputTokens, int outputTokens)
                            {
                                Interlocked.Add(ref _totalInputTokens, inputTokens);
                                Interlocked.Add(ref _totalOutputTokens, outputTokens);
                            }

                            public static void Clear()
                            {
                                _agentRuns.Clear();
                                _functionCalls.Clear();
                                _guardrailBlocks.Clear();
                                Interlocked.Exchange(ref _totalInputTokens, 0);
                                Interlocked.Exchange(ref _totalOutputTokens, 0);
                            }

                            public static void PrintReport()
                            {
                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine("\n┌─────────────────────────────────────────────────────────┐");
                                Console.WriteLine("│  📊 MIDDLEWARE METRICS : Raport pipeline'u              │");
                                Console.WriteLine("├─────────────────────────────────────────────────────────┤");

                                Console.WriteLine("│  Agent Runs:                                            │");
                                foreach (var run in _agentRuns.OrderBy(r => r.Timestamp))
                                {
                                    var icon = run.Success ? "✅" : "❌";
                                    Console.WriteLine($"│    {icon} {run.AgentName,-18} {run.Duration.TotalMilliseconds,7:F0}ms │");
                                }

                                if (!_functionCalls.IsEmpty)
                                {
                                    Console.WriteLine("│                                                         │");
                                    Console.WriteLine("│  Function Calls:                                        │");
                                    foreach (var call in _functionCalls.OrderBy(c => c.Timestamp))
                                    {
                                        Console.WriteLine($"│    🔧 {call.AgentName}/{call.FunctionName,-16} " +
                                            $"{call.Duration.TotalMilliseconds,5:F0}ms │");
                                    }
                                }

                                if (!_guardrailBlocks.IsEmpty)
                                {
                                    Console.WriteLine("│                                                         │");
                                    Console.WriteLine("│  Guardrail Blocks:                                      │");
                                    var grouped = _guardrailBlocks.GroupBy(b => b).Select(g => (g.Key, g.Count()));
                                    foreach (var (reason, count) in grouped)
                                    {
                                        Console.WriteLine($"│    🛡️  {reason,-20} ×{count}                     │");
                                    }
                                }

                                Console.WriteLine("│                                                         │");
                                Console.WriteLine($"│  Tokeny (szacunkowo):                                   │");
                                Console.WriteLine($"│    Input:  ~{Interlocked.Read(ref _totalInputTokens),8} tokenów               │");
                                Console.WriteLine($"│    Output: ~{Interlocked.Read(ref _totalOutputTokens),8} tokenów               │");
                                Console.WriteLine($"│    Razem:  ~{Interlocked.Read(ref _totalInputTokens) + Interlocked.Read(ref _totalOutputTokens),8} tokenów               │");

                                Console.WriteLine("└─────────────────────────────────────────────────────────┘");
                                Console.ResetColor();
                            }
                        }
                    
                

ContentGuardrails.cs : Guardrails pipeline

                    
                        // ============================================================================
                        //
                        //   1. ContentPolicyGuardrail : redaktuje dane wrażliwe z output (PII)
                        //   2. BrandSafetyGuardrail  : wzmianki o konkurencji, CTA check
                        //   3. OutputLengthGuardrail : obcina nadmiernie długi output
                        //
                        //   GuardrailFactory : composable wrapping agentów z pełnym pipeline'em
                        //
                        // ============================================================================

                        using Microsoft.Agents.AI;
                        using Microsoft.Extensions.AI;
                        using System.Text.RegularExpressions;
                        using AiContentTeam.Middleware;

                        namespace AiContentTeam.Guardrails;

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  GUARDRAIL ABSTRAKCJA
                        // ══════════════════════════════════════════════════════════════════════════════

                        public abstract class GuardrailBase
                        {
                            public string Name { get; }

                            protected GuardrailBase(string name) => Name = name;

                            public async Task<AgentResponse> ExecuteAsync(
                                IEnumerable<ChatMessage> messages,
                                AgentSession? session,
                                AgentRunOptions? options,
                                AIAgent innerAgent,
                                CancellationToken cancellationToken)
                            {
                                // ── Pre-execution check ──
                                var preResult = await CheckInputAsync(messages, innerAgent, cancellationToken);
                                if (preResult is not null)
                                {
                                    Console.ForegroundColor = ConsoleColor.Red;
                                    Console.WriteLine($"  [Guardrail:{Name}] 🛑 INPUT ZABLOKOWANY: {preResult.Reason}");
                                    Console.ResetColor();

                                    MiddlewareMetrics.RecordGuardrailBlock($"{Name}:pre:{preResult.Reason}");

                                    return new AgentResponse([
                                        new ChatMessage(ChatRole.Assistant,
                                            $"⚠️ Guardrail '{Name}': {preResult.Reason}")
                                    ]);
                                }

                                // ── Agent execution ──
                                var response = await innerAgent.RunAsync(
                                    messages, session, options, cancellationToken).ConfigureAwait(false);

                                // ── Post-execution check ──
                                var postResult = await CheckOutputAsync(response, innerAgent, cancellationToken);

                                return postResult switch
                                {
                                    { Action: GuardrailAction.Block } => HandleBlock(postResult),
                                    { Action: GuardrailAction.Modify } => HandleModify(response, postResult),
                                    { Action: GuardrailAction.Warn } => HandleWarn(response, postResult),
                                    _ => response
                                };
                            }

                            protected virtual Task<GuardrailViolation?> CheckInputAsync(
                                IEnumerable<ChatMessage> messages, AIAgent agent, CancellationToken ct)
                                => Task.FromResult<GuardrailViolation?>(null);

                            protected virtual Task<GuardrailViolation?> CheckOutputAsync(
                                AgentResponse response, AIAgent agent, CancellationToken ct)
                                => Task.FromResult<GuardrailViolation?>(null);

                            private AgentResponse HandleBlock(GuardrailViolation violation)
                            {
                                Console.ForegroundColor = ConsoleColor.Red;
                                Console.WriteLine($"  [Guardrail:{Name}] 🛑 OUTPUT ZABLOKOWANY: {violation.Reason}");
                                Console.ResetColor();
                                MiddlewareMetrics.RecordGuardrailBlock($"{Name}:post:{violation.Reason}");

                                return new AgentResponse([
                                    new ChatMessage(ChatRole.Assistant,
                                        $"⚠️ Guardrail '{Name}': Output zablokowany : {violation.Reason}")
                                ]);
                            }

                            private AgentResponse HandleModify(AgentResponse original, GuardrailViolation violation)
                            {
                                Console.ForegroundColor = ConsoleColor.Yellow;
                                Console.WriteLine($"  [Guardrail:{Name}] ✏️ OUTPUT ZMODYFIKOWANY: {violation.Reason}");
                                Console.ResetColor();
                                MiddlewareMetrics.RecordGuardrailBlock($"{Name}:modify:{violation.Reason}");

                                if (violation.ModifiedText is not null)
                                    return new AgentResponse([new ChatMessage(ChatRole.Assistant, violation.ModifiedText)]);

                                return original;
                            }

                            private AgentResponse HandleWarn(AgentResponse original, GuardrailViolation violation)
                            {
                                Console.ForegroundColor = ConsoleColor.DarkYellow;
                                Console.WriteLine($"  [Guardrail:{Name}] ⚠️ OSTRZEŻENIE: {violation.Reason}");
                                Console.ResetColor();
                                return original;
                            }
                        }

                        public enum GuardrailAction { Block, Modify, Warn }

                        public sealed record GuardrailViolation(
                            string Reason,
                            GuardrailAction Action = GuardrailAction.Block,
                            string? ModifiedText = null);

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  GUARDRAIL: Content Policy
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Post-execution: redaktuje dane wrażliwe z output (telefony, emaile).
                        /// Ostrzega o nadmiarze emoji.
                        /// </summary>
                        public sealed class ContentPolicyGuardrail : GuardrailBase
                        {
                            public ContentPolicyGuardrail() : base("ContentPolicy") { }

                            protected override Task<GuardrailViolation?> CheckOutputAsync(
                                AgentResponse response, AIAgent agent, CancellationToken ct)
                            {
                                var text = response.Text ?? "";

                                // Redakcja numerów telefonów
                                if (Regex.IsMatch(text, @"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b"))
                                    return Task.FromResult<GuardrailViolation?>(
                                        new("Output zawiera numer telefonu", GuardrailAction.Modify,
                                            Regex.Replace(text, @"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "[PHONE REDACTED]")));

                                // Redakcja emaili
                                if (Regex.IsMatch(text, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"))
                                    return Task.FromResult<GuardrailViolation?>(
                                        new("Output zawiera adres email", GuardrailAction.Modify,
                                            Regex.Replace(text, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
                                                "[EMAIL REDACTED]")));

                                // Ostrzeżenie o nadmiarze emoji
                                var emojiCount = Regex.Matches(text, @"[\uD83D][\uDE00-\uDE4F\uDE80-\uDEFF]|[\uD83E][\uDD00-\uDDFF]").Count;
                                if (emojiCount > 5)
                                    return Task.FromResult<GuardrailViolation?>(
                                        new($"Zbyt wiele emoji ({emojiCount}), max 5", GuardrailAction.Warn));

                                return Task.FromResult<GuardrailViolation?>(null);
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  GUARDRAIL: Brand Safety
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Sprawdza wzmianki o konkurencji i obecność CTA w poście.
                        /// </summary>
                        public sealed class BrandSafetyGuardrail : GuardrailBase
                        {
                            private readonly HashSet<string> _competitors;
                            private readonly bool _requireCTA;

                            public BrandSafetyGuardrail(
                                IEnumerable<string>? competitors = null,
                                bool requireCTA = false)
                                : base("BrandSafety")
                            {
                                _competitors = new HashSet<string>(
                                    competitors ?? [], StringComparer.OrdinalIgnoreCase);
                                _requireCTA = requireCTA;
                            }

                            protected override Task<GuardrailViolation?> CheckOutputAsync(
                                AgentResponse response, AIAgent agent, CancellationToken ct)
                            {
                                var text = response.Text ?? "";

                                foreach (var competitor in _competitors)
                                {
                                    if (text.Contains(competitor, StringComparison.OrdinalIgnoreCase))
                                        return Task.FromResult<GuardrailViolation?>(
                                            new($"Wzmianka o konkurencji: '{competitor}'", GuardrailAction.Warn));
                                }

                                if (_requireCTA && !text.Contains('?'))
                                    return Task.FromResult<GuardrailViolation?>(
                                        new("Post nie zawiera pytania/CTA", GuardrailAction.Warn));

                                return Task.FromResult<GuardrailViolation?>(null);
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  GUARDRAIL: Output Length
                        // ══════════════════════════════════════════════════════════════════════════════

                        public sealed class OutputLengthGuardrail : GuardrailBase
                        {
                            private readonly int _maxChars;

                            public OutputLengthGuardrail(int maxChars = 3000) : base("OutputLength")
                                => _maxChars = maxChars;

                            protected override Task<GuardrailViolation?> CheckOutputAsync(
                                AgentResponse response, AIAgent agent, CancellationToken ct)
                            {
                                var text = response.Text ?? "";
                                if (text.Length > _maxChars)
                                {
                                    return Task.FromResult<GuardrailViolation?>(
                                        new($"Output za długi ({text.Length}/{_maxChars})",
                                            GuardrailAction.Modify,
                                            text[.._maxChars] + $"\n\n[... obcięto z {text.Length} do {_maxChars} znaków]"));
                                }
                                return Task.FromResult<GuardrailViolation?>(null);
                            }
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  FACTORY
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Factory do composable'owego opakowywania agentów middleware + guardrails.
                        /// 
                        /// Kolejność warstw (od zewnątrz):
                        ///   Logging → Retry → Guardrails → FunctionAudit → FunctionValidation → Agent
                        /// </summary>
                        public static class GuardrailFactory
                        {
                            /// <summary>
                            /// Pełny pipeline: Logging + Retry + Function middleware + Guardrails.
                            /// </summary>
                            public static AIAgent WrapWithFullPipeline(
                                AIAgent agent,
                                bool enableLogging = true,
                                bool enableRetry = true,
                                params GuardrailBase[] guardrails)
                            {
                                var builder = agent.AsBuilder();

                                // Warstwa 1 (najbardziej zewnętrzna): Logging
                                if (enableLogging)
                                {
                                    builder.Use(
                                        runFunc: LoggingMiddleware.RunAsync,
                                        runStreamingFunc: LoggingMiddleware.RunStreamingAsync);
                                }

                                // Warstwa 2: Retry
                                if (enableRetry)
                                {
                                    builder.Use(
                                        runFunc: RetryMiddleware.RunAsync,
                                        runStreamingFunc: null);
                                }

                                // Warstwa 3: Token tracking
                                builder.Use(
                                    runFunc: TokenTrackingMiddleware.RunAsync,
                                    runStreamingFunc: null);

                                // Warstwa 4: Guardrails (każdy jako osobna warstwa)
                                foreach (var guardrail in guardrails)
                                {
                                    builder.Use(
                                        runFunc: guardrail.ExecuteAsync,
                                        runStreamingFunc: null);
                                }

                                // Warstwa 4 (najbliżej agenta): Function calling middleware
                                builder.Use(AuditFunctionMiddleware.InvokeAsync);
                                builder.Use(InputValidationMiddleware.InvokeAsync);

                                return builder.Build();
                            }
                        }
                    
                

ResearcherExecutor.cs

                    
                        // ============================================================================
                        // Przyjmuje AIAgent z zewnątrz : middleware/guardrails podpinane w Program.cs
                        // ============================================================================

                        using Microsoft.Agents.AI;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;

                        namespace AiContentTeam;

                        // ── Typy danych ──────────────────────────────────────────────────────────────

                        public sealed record ResearchRequest(
                            string YouTubeUrl,
                            string TargetAudience);

                        public sealed record ResearchResult(
                            string YouTubeUrl,
                            string[] KeyPoints,
                            string TargetAudience);

                        // ── Custom event ─────────────────────────────────────────────────────────────

                        public sealed class ResearchCompletedEvent(ResearchResult result) : WorkflowEvent(result)
                        {
                            public ResearchResult Result => result;
                            public override string ToString() =>
                                $"[Research] Wyodrębniono {result.KeyPoints.Length} tez z: {result.YouTubeUrl}";
                        }

                        // ── Executor ─────────────────────────────────────────────────────────────────

                        [YieldsOutput(typeof(ResearchResult))]
                        public partial class ResearcherExecutor : Executor<ResearchRequest, ResearchResult>
                        {
                            private readonly AIAgent _agent;
                            private AgentSession? _session;

                            /// <summary>
                            /// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
                            /// </summary>
                            public ResearcherExecutor(AIAgent agent) : base("Researcher")
                            {
                                _agent = agent;
                            }

                            public override async ValueTask<ResearchResult> HandleAsync(
                                ResearchRequest message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                await context.AddEventAsync(
                                    new AgentProgressEvent("Researcher", $"Pobieram transkrypcję z: {message.YouTubeUrl}"),
                                    cancellationToken);

                                _session ??= await _agent.CreateSessionAsync(cancellationToken);

                                var prompt = $"""
                                    Pobierz transkrypcję i wyodrębnij tezy z: {message.YouTubeUrl}
                                    Grupa docelowa: {message.TargetAudience}
                                    """;

                                var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);

                                var keyPoints = agentResult.Text
                                    .Split('\n', StringSplitOptions.RemoveEmptyEntries)
                                    .Where(line => line.TrimStart().StartsWith("• "))
                                    .Select(line => line.TrimStart().TrimStart('•').Trim())
                                    .ToArray();

                                var result = new ResearchResult(
                                    YouTubeUrl: message.YouTubeUrl,
                                    KeyPoints: keyPoints,
                                    TargetAudience: message.TargetAudience);

                                await context.AddEventAsync(new ResearchCompletedEvent(result), cancellationToken);

                                return result;
                            }
                        }
                    
                

ApprovalExecutor.cs : Reusable HITL via RequestPort

                    
                        // ============================================================================
                        //
                        // Wzorzec z oficjalnej dokumentacji MAF (Expense Reimbursement blog):
                        //   PrepareExecutor → RequestPort → GateExecutor → next
                        //
                        // RequestPort JEST executorem w grafie (wewnętrznie tworzy RequestInfoExecutor).
                        // Nie można mieć innego executora z tym samym ID.
                        // Dlatego dzielimy na:
                        //
                        //   1. PrepareApprovalExecutor<TData>  : konwertuje TData → ApprovalRequest
                        //      (edge: prepare → port : port emituje RequestInfoEvent i blokuje)
                        //
                        //   2. ApprovalGateExecutor<TData>  : odbiera ApprovalDecision z portu,
                        //      loguje audit, sprawdza timeout, przepuszcza lub przerywa
                        //      (edge: port → gate → next)
                        //
                        // Features:
                        //   A. Timeout + auto-reject
                        //   B. Audit log (każda decyzja logowana, walidacja post-mortem)
                        //   C. Reusable factory: ApprovalStage.Create<T>(...)
                        // ============================================================================

                        using Microsoft.Agents.AI.Workflows;
                        using System.Collections.Concurrent;

                        namespace AiContentTeam;

                        // ── Typy zatwierdzeń ──────────────────────────────────────────────────────────

                        /// <summary>
                        /// Żądanie zatwierdzenia wysyłane przez RequestPort do człowieka.
                        /// </summary>
                        public sealed record ApprovalRequest(
                            string PhaseName,
                            string Summary,
                            DateTime RequestedAt)
                        {
                            public ApprovalRequest(string phaseName, string summary)
                                : this(phaseName, summary, DateTime.UtcNow) { }
                        }

                        /// <summary>
                        /// Odpowiedź od człowieka: approved/rejected + opcjonalny feedback.
                        /// </summary>
                        public sealed record ApprovalDecision(
                            bool Approved,
                            string? Feedback = null);

                        // ── Audit log ─────────────────────────────────────────────────────────────────

                        public sealed record ApprovalAuditEntry(
                            string PhaseName,
                            DateTime RequestedAt,
                            DateTime DecidedAt,
                            bool Approved,
                            string? Feedback,
                            bool TimedOut);

                        /// <summary>
                        /// Globalny, thread-safe audit log zatwierdzeń.
                        /// Waliduje że KAŻDA faza HITL się wykonała.
                        /// </summary>
                        public static class ApprovalAuditLog
                        {
                            private static readonly ConcurrentBag<ApprovalAuditEntry> _entries = [];

                            public static void Record(ApprovalAuditEntry entry) => _entries.Add(entry);
                            public static IReadOnlyList<ApprovalAuditEntry> GetAll() => [.. _entries];

                            public static bool AllPhasesApproved(params string[] requiredPhases)
                            {
                                var approved = new HashSet<string>(
                                    _entries.Where(e => e.Approved).Select(e => e.PhaseName));
                                return requiredPhases.All(approved.Contains);
                            }

                            public static void Clear() => _entries.Clear();

                            public static void PrintReport()
                            {
                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine("\n┌─────────────────────────────────────────────────────────┐");
                                Console.WriteLine("│  📋 AUDIT LOG : Historia zatwierdzeń HITL               │");
                                Console.WriteLine("├─────────────────────────────────────────────────────────┤");

                                foreach (var entry in _entries.OrderBy(e => e.RequestedAt))
                                {
                                    var icon = entry.Approved ? "✅" : "❌";
                                    var timeout = entry.TimedOut ? " (TIMEOUT)" : "";
                                    var duration = (entry.DecidedAt - entry.RequestedAt).TotalMilliseconds;
                                    Console.WriteLine($"│  {icon} {entry.PhaseName,-20} " +
                                        $"{duration,6:F0}ms {timeout}");
                                    if (!string.IsNullOrEmpty(entry.Feedback))
                                        Console.WriteLine($"│     💬 {entry.Feedback}");
                                }

                                Console.WriteLine("└─────────────────────────────────────────────────────────┘");
                                Console.ResetColor();
                            }
                        }

                        // ── Custom event ──────────────────────────────────────────────────────────────

                        public sealed class ApprovalCompletedEvent(string phaseName, bool approved, string? feedback)
                            : WorkflowEvent($"{phaseName}: {(approved ? "approved" : "rejected")}")
                        {
                            public string PhaseName => phaseName;
                            public bool Approved => approved;
                            public string? Feedback => feedback;
                        }

                        // ── PrepareApprovalExecutor<TData> ────────────────────────────────────────────

                        /// <summary>
                        /// Konwertuje TData na ApprovalRequest.
                        /// Edge do RequestPort sprawia, że port emituje RequestInfoEvent i blokuje workflow.
                        ///
                        /// Graf: previousExecutor → PrepareApproval → RequestPort (blokuje!)
                        /// </summary>
                        [YieldsOutput(typeof(ApprovalRequest))]
                        public partial class PrepareApprovalExecutor<TData> : Executor<TData, ApprovalRequest> where TData : class
                        {
                            private readonly string _phaseName;
                            private readonly Func<TData, string> _summarizer;

                            /// <summary>Dane czekające na zatwierdzenie : GateExecutor je odczytuje.</summary>
                            internal TData? PendingData { get; private set; }
                            internal DateTime RequestedAt { get; private set; }

                            public PrepareApprovalExecutor(string phaseName, Func<TData, string> summarizer)
                                : base($"Prepare{phaseName}")
                            {
                                _phaseName = phaseName;
                                _summarizer = summarizer;
                            }

                            public override async ValueTask<ApprovalRequest> HandleAsync(
                                TData message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                PendingData = message;
                                RequestedAt = DateTime.UtcNow;

                                var summary = _summarizer(message);

                                await context.AddEventAsync(
                                    new AgentProgressEvent($"Prepare{_phaseName}",
                                        $"⏸️  Czekam na zatwierdzenie fazy: {_phaseName}"),
                                    cancellationToken);

                                return new ApprovalRequest(_phaseName, summary);
                            }
                        }

                        // ── ApprovalGateExecutor<TData> ───────────────────────────────────────────────

                        /// <summary>
                        /// Odbiera ApprovalDecision z RequestPort.
                        /// Sprawdza timeout, loguje audit, przepuszcza TData dalej lub przerywa.
                        ///
                        /// Graf: RequestPort → GateExecutor → nextExecutor
                        /// </summary>
                        [SendsMessage(typeof(object))]
                        public partial class ApprovalGateExecutor<TData> : Executor<ApprovalDecision> where TData : class
                        {
                            private readonly string _phaseName;
                            private readonly PrepareApprovalExecutor<TData> _prepare;
                            private readonly TimeSpan _timeout;

                            public ApprovalGateExecutor(
                                string phaseName,
                                PrepareApprovalExecutor<TData> prepare,
                                TimeSpan? timeout = null)
                                : base($"Gate{phaseName}")
                            {
                                _phaseName = phaseName;
                                _prepare = prepare;
                                _timeout = timeout ?? TimeSpan.FromMinutes(5);
                            }

                            public override async ValueTask HandleAsync(
                                ApprovalDecision decision,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                var decidedAt = DateTime.UtcNow;
                                var timedOut = (decidedAt - _prepare.RequestedAt) > _timeout;

                                var finalDecision = timedOut
                                    ? new ApprovalDecision(false, $"Auto-reject: timeout po {_timeout.TotalSeconds}s")
                                    : decision;

                                // ── Audit log ─────────────────────────────────────────────────
                                ApprovalAuditLog.Record(new ApprovalAuditEntry(
                                    PhaseName: _phaseName,
                                    RequestedAt: _prepare.RequestedAt,
                                    DecidedAt: decidedAt,
                                    Approved: finalDecision.Approved,
                                    Feedback: finalDecision.Feedback,
                                    TimedOut: timedOut));

                                await context.AddEventAsync(
                                    new ApprovalCompletedEvent(_phaseName, finalDecision.Approved, finalDecision.Feedback),
                                    cancellationToken);

                                if (finalDecision.Approved && _prepare.PendingData is not null)
                                {
                                    await context.SendMessageAsync(_prepare.PendingData, cancellationToken: cancellationToken);
                                }
                                else
                                {
                                    var reason = finalDecision.Feedback ?? "Brak powodu";
                                    await context.YieldOutputAsync(
                                        $"❌ Faza '{_phaseName}' odrzucona: {reason}",
                                        cancellationToken);
                                }
                            }
                        }

                        // ── Factory ───────────────────────────────────────────────────────────────────

                        /// <summary>
                        /// Tworzy trójkę (Prepare, Port, Gate) do wstawienia w workflow graph.
                        /// 
                        /// Użycie:
                        ///   var research = ApprovalStage.Create&lt;ResearchResult&gt;(
                        ///       "Research", r =&gt; $"Tezy: {r.KeyPoints.Length}");
                        ///   
                        ///   builder
                        ///       .AddEdge(researcher, research.Prepare)
                        ///       .AddEdge(research.Prepare, research.Port)
                        ///       .AddEdge(research.Port, research.Gate)
                        ///       .AddEdge(research.Gate, nextExecutor)
                        /// </summary>
                        public static class ApprovalStage
                        {
                            public static (
                                PrepareApprovalExecutor<TData> Prepare,
                                ExecutorBinding Port,
                                ApprovalGateExecutor<TData> Gate)
                                Create<TData>(
                                    string phaseName,
                                    Func<TData, string> summarizer,
                                    TimeSpan? timeout = null) where TData : class
                            {
                                var prepare = new PrepareApprovalExecutor<TData>(phaseName, summarizer);
                                var port = RequestPort.Create<ApprovalRequest, ApprovalDecision>($"{phaseName}Port");
                                var gate = new ApprovalGateExecutor<TData>(phaseName, prepare, timeout);

                                return (prepare, port, gate);
                            }
                        }
                                            
                

CopywriterExecutor + StrategExecutor : przyjmują AIAgent z zewnątrz

                    
                        using Microsoft.Agents.AI;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;

                        namespace AiContentTeam;

                        // ── Typy danych ──────────────────────────────────────────────────────────────

                        public sealed record DraftPost(
                            string AgentName,
                            string Content);

                        public sealed record StrategyAnalysis(
                            string RecommendedHook,
                            string[] SuggestedHashtags,
                            string EngagementNotes);

                        // ── Custom eventy ─────────────────────────────────────────────────────────────

                        public sealed class DraftReadyEvent(DraftPost draft) : WorkflowEvent(draft)
                        {
                            public DraftPost Result => draft;
                            public override string ToString() =>
                                $"[Copywriter] Draft gotowy ({draft.Content.Length} znaków)";
                        }

                        public sealed class StrategyReadyEvent(StrategyAnalysis analysis) : WorkflowEvent(analysis)
                        {
                            public override string ToString() =>
                                $"[Strateg] Analiza gotowa : hook: \"{analysis.RecommendedHook}\"";
                        }

                        public sealed class AgentProgressEvent(string agentName, string status) : WorkflowEvent($"{agentName}: {status}")
                        {
                            public string AgentName => agentName;
                            public string Status => status;
                            public override string ToString() => $"[{agentName}] {status}";
                        }

                        // ── Shared State ──────────────────────────────────────────────────────────────

                        internal static class ResearchStateScope
                        {
                            public const string Name = "ResearchState";
                            public const string KeyPointsKey = "key_points";
                            public const string TargetAudienceKey = "target_audience";
                        }

                        // ── CopywriterExecutor ────────────────────────────────────────────────────────

                        [YieldsOutput(typeof(DraftPost))]
                        public partial class CopywriterExecutor : Executor<ResearchResult, DraftPost>
                        {
                            private readonly AIAgent _agent;
                            private AgentSession? _session;

                            /// <summary>
                            /// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
                            /// </summary>
                            public CopywriterExecutor(AIAgent agent) : base("Copywriter")
                            {
                                _agent = agent;
                            }

                            public override async ValueTask<DraftPost> HandleAsync(
                                ResearchResult message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                await context.QueueStateUpdateAsync(
                                    ResearchStateScope.KeyPointsKey,
                                    string.Join("\n", message.KeyPoints.Select(p => $"• {p}")),
                                    scopeName: ResearchStateScope.Name,
                                    cancellationToken);

                                await context.AddEventAsync(
                                    new AgentProgressEvent("Copywriter", "Generuję draft posta na LinkedIn..."),
                                    cancellationToken);

                                _session ??= await _agent.CreateSessionAsync(cancellationToken);

                                var prompt = $"""
                                    Tezy z researchu (dla grupy: {message.TargetAudience}):
                                    {string.Join("\n", message.KeyPoints.Select(p => $"• {p}"))}

                                    Napisz post na LinkedIn.
                                    """;

                                var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);
                                var draft = new DraftPost("Copywriter", agentResult.Text);

                                await context.AddEventAsync(new DraftReadyEvent(draft), cancellationToken);

                                return draft;
                            }
                        }

                        // ── StrategExecutor ───────────────────────────────────────────────────────────

                        [YieldsOutput(typeof(StrategyAnalysis))]
                        public partial class StrategExecutor : Executor<ResearchResult, StrategyAnalysis>
                        {
                            private readonly AIAgent _agent;
                            private AgentSession? _session;

                            /// <summary>
                            /// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
                            /// </summary>
                            public StrategExecutor(AIAgent agent) : base("Strateg")
                            {
                                _agent = agent;
                            }

                            public override async ValueTask<StrategyAnalysis> HandleAsync(
                                ResearchResult message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                await context.AddEventAsync(
                                    new AgentProgressEvent("Strateg", "Analizuję trendy i przygotowuję strategię..."),
                                    cancellationToken);

                                _session ??= await _agent.CreateSessionAsync(cancellationToken);

                                var prompt = $"""
                                    Temat: wyodrębnione tezy z materiału wideo dla grupy "{message.TargetAudience}":
                                    {string.Join("\n", message.KeyPoints.Select(p => $"• {p}"))}

                                    Sprawdź trendy i podaj analizę strategiczną.
                                    """;

                                var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);

                                var lines = agentResult.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
                                var hook = lines.FirstOrDefault(l => l.StartsWith("REKOMENDOWANY HOOK:"))
                                            ?.Replace("REKOMENDOWANY HOOK:", "").Trim()
                                        ?? lines.FirstOrDefault() ?? "Odkryj nowe możliwości";

                                var hashtags = lines.FirstOrDefault(l => l.StartsWith("SUGEROWANE HASHTAGI:"))
                                                ?.Replace("SUGEROWANE HASHTAGI:", "").Trim()
                                                .Split(' ', StringSplitOptions.RemoveEmptyEntries)
                                                .Where(h => h.StartsWith("#"))
                                                .ToArray()
                                            ?? ["#dotnet", "#ai"];

                                var notes = lines.FirstOrDefault(l => l.StartsWith("UWAGI O ZAANGAŻOWANIU:"))
                                                ?.Replace("UWAGI O ZAANGAŻOWANIU:", "").Trim()
                                            ?? "Brak dodatkowych uwag.";

                                var analysis = new StrategyAnalysis(hook, hashtags, notes);

                                await context.AddEventAsync(new StrategyReadyEvent(analysis), cancellationToken);

                                return analysis;
                            }
                        }
                    
                

RedaktorExecutor.cs

                    
                        // ============================================================================
                        // RedaktorExecutor przyjmuje AIAgent z zewnątrz
                        // ============================================================================

                        using Microsoft.Agents.AI;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;

                        namespace AiContentTeam;

                        // ── Typy ──────────────────────────────────────────────────────────────────────

                        public sealed record EditorialInput(
                            DraftPost Draft,
                            StrategyAnalysis Strategy);

                        public sealed record FinalPost(
                            string Content,
                            string[] Hashtags,
                            string EditedAt);

                        // ── Custom event ──────────────────────────────────────────────────────────────

                        public sealed class PostPublishedEvent(string content, string publishedAt) : WorkflowEvent(content)
                        {
                            public string PublishedAt => publishedAt;
                            public override string ToString() => $"[Publish] Post opublikowany o {publishedAt}";
                        }

                        // ── AggregatorExecutor ────────────────────────────────────────────────────────

                        [SendsMessage(typeof(EditorialInput))]
                        public partial class AggregatorExecutor() : Executor<object>("Aggregator")
                        {
                            private DraftPost? _draft;
                            private StrategyAnalysis? _strategy;

                            public override async ValueTask HandleAsync(
                                object message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                if (message is DraftPost d) _draft = d;
                                if (message is StrategyAnalysis s) _strategy = s;

                                if (_draft is not null && _strategy is not null)
                                    await context.SendMessageAsync(
                                        new EditorialInput(_draft, _strategy),
                                        cancellationToken: cancellationToken);
                            }
                        }

                        // ── RedaktorExecutor ──────────────────────────────────────────────────────────

                        [YieldsOutput(typeof(FinalPost))]
                        public partial class RedaktorExecutor : Executor<EditorialInput, FinalPost>
                        {
                            private readonly AIAgent _agent;
                            private AgentSession? _session;

                            /// <summary>
                            /// Przyjmuje gotowego AIAgent : middleware i guardrails podpięte w Program.cs.
                            /// </summary>
                            public RedaktorExecutor(AIAgent agent) : base("Redaktor")
                            {
                                _agent = agent;
                            }

                            public override async ValueTask<FinalPost> HandleAsync(
                                EditorialInput message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                await context.AddEventAsync(
                                    new AgentProgressEvent("Redaktor", "Scala draft i strategię, przygotowuje finalny post..."),
                                    cancellationToken);

                                _session ??= await _agent.CreateSessionAsync(cancellationToken);

                                var prompt = $"""
                                    DRAFT POSTA (od Copywritera):
                                    {message.Draft.Content}

                                    ANALIZA STRATEGICZNA (od Stratega):
                                    Rekomendowany hook: {message.Strategy.RecommendedHook}
                                    Sugerowane hashtagi: {string.Join(" ", message.Strategy.SuggestedHashtags)}
                                    Uwagi: {message.Strategy.EngagementNotes}

                                    Popraw draft zgodnie z analizą. Napisz TYLKO finalny tekst posta.
                                    """;

                                var agentResult = await _agent.RunAsync(prompt, _session, cancellationToken: cancellationToken);

                                return new FinalPost(
                                    Content: agentResult.Text,
                                    Hashtags: message.Strategy.SuggestedHashtags,
                                    EditedAt: DateTime.Now.ToString("HH:mm:ss"));
                            }
                        }

                        // ── PublishExecutor ───────────────────────────────────────────────────────────

                        [YieldsOutput(typeof(string))]
                        public partial class PublishExecutor() : Executor<FinalPost>("Publish")
                        {
                            public override async ValueTask HandleAsync(
                                FinalPost message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                Console.ForegroundColor = ConsoleColor.Green;
                                Console.WriteLine("\n✅ POST OPUBLIKOWANY NA LINKEDIN:");
                                Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                                Console.WriteLine(message.Content);
                                Console.WriteLine($"\n{string.Join(" ", message.Hashtags)}");
                                Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
                                Console.ResetColor();

                                var publishedAt = DateTime.Now.ToString("HH:mm:ss");

                                await context.AddEventAsync(
                                    new PostPublishedEvent(message.Content, publishedAt),
                                    cancellationToken);

                                await context.YieldOutputAsync(
                                    $"Opublikowano o {publishedAt}",
                                    cancellationToken);
                            }
                        }
                    
                

ConsoleWorkflowRenderer.cs

                    
                        // ============================================================================
                        // Wszystkie 3 HITL via RequestPort : zero zależności od agenta
                        // ============================================================================

                        using AiContentTeam;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;
                        using System.Diagnostics;

                        sealed class ConsoleWorkflowRenderer
                        {
                            private readonly string _finalAgentName;
                            private readonly HashSet<string> _parallelAgents;
                            private readonly HashSet<string> _seenAgents = new();
                            private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
                            private readonly Stopwatch _phaseStopwatch = new();
                            private bool _workflowOutputReceived;
                            private bool _parallelBannerShown;

                            public ConsoleWorkflowRenderer(string finalAgentName, HashSet<string> parallelAgents)
                            {
                                _finalAgentName = finalAgentName;
                                _parallelAgents = parallelAgents;
                            }

                            // ── Style agentów ─────────────────────────────────────────────────────────

                            private static readonly Dictionary<string, (ConsoleColor Color, string Icon, string Prefix)> AgentStyles = new()
                            {
                                ["Researcher"] = (ConsoleColor.Cyan, "🔍", "→"),
                                ["Copywriter"] = (ConsoleColor.Yellow, "✍️ ", "║ PARALLEL ║"),
                                ["Strateg"] = (ConsoleColor.Blue, "📊", "║ PARALLEL ║"),
                                ["Redaktor"] = (ConsoleColor.Magenta, "📝", "→"),
                                ["Publish"] = (ConsoleColor.Green, "🚀", "→"),
                                ["PrepareResearch"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
                                ["PrepareEditorial"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
                                ["PreparePublish"] = (ConsoleColor.DarkYellow, "🛑", "→ HITL"),
                            };

                            private static (ConsoleColor Color, string Icon, string Prefix) GetStyle(string agentName)
                                => AgentStyles.GetValueOrDefault(agentName, (ConsoleColor.White, "🤖", "→"));

                            private static string ShortName(string executorId)
                                => executorId.Split('_')[0];

                            // ── Progress handler ──────────────────────────────────────────────────────

                            public EventAction HandleAgentProgress(AgentProgressEvent evt)
                            {
                                var (color, icon, _) = GetStyle(evt.AgentName);
                                Console.ForegroundColor = color;
                                Console.WriteLine($"   ⏳ {icon} {evt.AgentName}: {evt.Status}");
                                Console.ResetColor();
                                return EventAction.Next;
                            }

                            // ── Approval completed ────────────────────────────────────────────────────

                            public EventAction HandleApprovalCompleted(ApprovalCompletedEvent evt)
                            {
                                Console.ForegroundColor = evt.Approved ? ConsoleColor.Green : ConsoleColor.Red;
                                var icon = evt.Approved ? "✅" : "❌";
                                Console.WriteLine($"\n   {icon} HITL {evt.PhaseName}: {(evt.Approved ? "ZATWIERDZONO" : "ODRZUCONO")}");
                                if (!string.IsNullOrEmpty(evt.Feedback))
                                {
                                    Console.ForegroundColor = ConsoleColor.DarkGray;
                                    Console.WriteLine($"   💬 Feedback: {evt.Feedback}");
                                }
                                Console.ResetColor();
                                return EventAction.Next;
                            }

                            // ── Custom event handlers ────────────────────────────────────────────────

                            public EventAction HandleResearchCompleted(ResearchCompletedEvent evt)
                            {
                                _phaseStopwatch.Stop();
                                Console.ResetColor();
                                Console.ForegroundColor = ConsoleColor.Cyan;
                                Console.WriteLine("\n┌─────────────────────────────────────────────────────────────┐");
                                Console.WriteLine($"│ 🔍 RESEARCH ZAKOŃCZONY : {evt.Result.KeyPoints.Length} tez wyodrębnionych");
                                Console.WriteLine("└─────────────────────────────────────────────────────────────┘");
                                Console.ResetColor();

                                foreach (var point in evt.Result.KeyPoints)
                                {
                                    Console.ForegroundColor = ConsoleColor.DarkCyan;
                                    Console.WriteLine($"   • {point}");
                                }
                                Console.ResetColor();
                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"   ⏱️  Faza 1 (Research): {_phaseStopwatch.ElapsedMilliseconds}ms");
                                Console.ResetColor();
                                _phaseStopwatch.Restart();

                                return EventAction.Next;
                            }

                            public EventAction HandleDraftReady(DraftReadyEvent evt)
                            {
                                Console.ResetColor();
                                Console.ForegroundColor = ConsoleColor.Yellow;
                                var length = evt.Result.Content.Length;
                                Console.WriteLine($"\n   ✓ Draft gotowy ({length} znaków)");

                                var preview = evt.Result.Content.Length > 200
                                    ? evt.Result.Content[..200] + "..."
                                    : evt.Result.Content;
                                Console.ForegroundColor = ConsoleColor.DarkYellow;
                                Console.WriteLine($"   ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
                                Console.WriteLine($"   {preview}");
                                Console.WriteLine($"   ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
                                Console.ResetColor();
                                return EventAction.Next;
                            }

                            public EventAction HandleStrategyReady(StrategyReadyEvent evt)
                            {
                                Console.ResetColor();
                                Console.ForegroundColor = ConsoleColor.Blue;
                                if (evt.Data is StrategyAnalysis analysis)
                                {
                                    Console.WriteLine($"\n   ✓ Analiza strategiczna gotowa");
                                    Console.ForegroundColor = ConsoleColor.DarkBlue;
                                    Console.WriteLine($"   Hook: \"{analysis.RecommendedHook}\"");
                                    if (analysis.SuggestedHashtags.Length > 0)
                                        Console.WriteLine($"   Hashtagi: {string.Join(" ", analysis.SuggestedHashtags)}");
                                    Console.WriteLine($"   Uwagi: {analysis.EngagementNotes}");
                                }
                                Console.ResetColor();
                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"   ⏱️  Faza 2 (równoległa): {_phaseStopwatch.ElapsedMilliseconds}ms");
                                Console.ResetColor();
                                _phaseStopwatch.Restart();

                                return EventAction.Next;
                            }

                            // ── Framework event handlers ──────────────────────────────────────────────

                            public EventAction HandleError(ExecutorFailedEvent evt)
                            {
                                Console.ForegroundColor = ConsoleColor.Red;
                                Console.WriteLine($"\n❌ BŁĄD AGENTA: {evt.ExecutorId}");
                                Console.WriteLine($"   {evt.Data}");
                                Console.ResetColor();
                                return EventAction.Break;
                            }

                            public EventAction HandleToken(AgentResponseUpdateEvent evt)
                            {
                                var name = ShortName(evt.ExecutorId);

                                if (_seenAgents.Add(evt.ExecutorId))
                                {
                                    PrintParallelBanner(name);
                                    PrintAgentHeader(name);
                                }

                                if (!string.IsNullOrEmpty(evt.Update?.Text))
                                    Console.Write(evt.Update.Text);

                                return EventAction.Next;
                            }

                            // ── HITL handler ──────────────────────────────────────────────────────────

                            /// <summary>
                            /// Obsługuje RequestInfoEvent z RequestPort (workflow-level HITL).
                            /// Payload = ApprovalRequest.
                            /// </summary>
                            public async Task<EventAction> HandleHITL(RequestInfoEvent evt, StreamingRun run)
                            {
                                if (evt.Request.TryGetDataAs(out ApprovalRequest? approvalRequest)
                                    && approvalRequest is not null)
                                {
                                    return await HandleWorkflowApproval(evt, run, approvalRequest);
                                }

                                return EventAction.Next;
                            }

                            private async Task<EventAction> HandleWorkflowApproval(
                                RequestInfoEvent evt, StreamingRun run, ApprovalRequest approvalRequest)
                            {
                                Console.ResetColor();
                                Console.WriteLine();
                                Console.ForegroundColor = ConsoleColor.DarkYellow;
                                Console.WriteLine("╔══════════════════════════════════════════════════════════════╗");
                                Console.WriteLine($"║  🛑 WORKFLOW HITL : Zatwierdzenie fazy: {approvalRequest.PhaseName,-18}║");
                                Console.WriteLine("╚══════════════════════════════════════════════════════════════╝");
                                Console.ResetColor();

                                Console.ForegroundColor = ConsoleColor.White;
                                Console.WriteLine($"\n📋 Podsumowanie:");
                                Console.ForegroundColor = ConsoleColor.Cyan;

                                // Pełne podsumowanie : każda linia z wcięciem
                                foreach (var line in approvalRequest.Summary.Split('\n'))
                                {
                                    Console.WriteLine($"   {line}");
                                }
                                Console.ResetColor();

                                Console.ForegroundColor = ConsoleColor.Yellow;
                                Console.Write($"\n👉 Zatwierdzić fazę '{approvalRequest.PhaseName}'? (t/n, opcjonalnie feedback po spacji): ");
                                Console.ResetColor();

                                var input = Console.ReadLine()?.Trim() ?? "";
                                var parts = input.Split(' ', 2);
                                var answer = parts[0].ToLower();
                                bool approved = answer is "t" or "tak" or "y" or "yes";
                                string? feedback = parts.Length > 1 ? parts[1] : null;

                                var approvalDecision = new ApprovalDecision(approved, feedback);

                                await run.SendResponseAsync(
                                    evt.Request.CreateResponse(approvalDecision));

                                Console.ForegroundColor = approved ? ConsoleColor.Green : ConsoleColor.Red;
                                Console.WriteLine(approved
                                    ? $"\n✅ Faza '{approvalRequest.PhaseName}' zatwierdzona!"
                                    : $"\n❌ Faza '{approvalRequest.PhaseName}' odrzucona.");
                                Console.ResetColor();

                                return EventAction.Next;
                            }

                            public EventAction HandleOutput(WorkflowOutputEvent evt)
                            {
                                if (ShortName(evt.ExecutorId ?? "") == _finalAgentName)
                                    _workflowOutputReceived = true;
                                return EventAction.Continue;
                            }

                            public EventAction HandleCompleted(ExecutorCompletedEvent evt)
                            {
                                var name = ShortName(evt.ExecutorId);

                                Console.ForegroundColor = ConsoleColor.DarkGray;
                                Console.WriteLine($"\n   ✔ {name} zakończony");
                                Console.ResetColor();

                                if (_workflowOutputReceived)
                                {
                                    _stopwatch.Stop();
                                    return EventAction.Break;
                                }
                                return EventAction.Next;
                            }

                            // ── Wyświetlanie ──────────────────────────────────────────────────────────

                            private void PrintParallelBanner(string agentName)
                            {
                                if (!_parallelBannerShown && _parallelAgents.Contains(agentName))
                                {
                                    _parallelBannerShown = true;
                                    Console.ForegroundColor = ConsoleColor.DarkYellow;
                                    Console.WriteLine("\n\n⚡ FAZA RÓWNOLEGŁA : Copywriter i Strateg startują jednocześnie:");
                                    Console.ResetColor();
                                }
                            }

                            private static void PrintAgentHeader(string agentName)
                            {
                                var (color, icon, prefix) = GetStyle(agentName);
                                Console.WriteLine();
                                Console.WriteLine("────────────────────────────────────────────────────────────────");
                                Console.ForegroundColor = color;
                                Console.WriteLine($" {prefix} AGENT: {agentName} {icon}");
                                Console.WriteLine("────────────────────────────────────────────────────────────────");
                                Console.ResetColor();
                            }

                            public void PrintSummary()
                            {
                                Console.ResetColor();
                                Console.WriteLine();
                                Console.WriteLine("════════════════════════════════════════════════════════════════════");
                                Console.ForegroundColor = ConsoleColor.Green;
                                Console.WriteLine($"🏁 Pipeline zakończony! Łączny czas: {_stopwatch.ElapsedMilliseconds}ms");
                                Console.ResetColor();
                                Console.WriteLine("════════════════════════════════════════════════════════════════════");
                                Console.WriteLine("\n💡 Copywriter i Strateg startowali jednocześnie :");
                                Console.WriteLine("   czas fazy 2 = max(t_copywriter, t_strateg)");

                                // ── Audit report ──────────────────────────────────────────────
                                ApprovalAuditLog.PrintReport();

                                // ── Walidacja wszystkich faz HITL ─────────────────────────────
                                var requiredPhases = new[] { "Research", "Editorial", "Publish" };
                                if (ApprovalAuditLog.AllPhasesApproved(requiredPhases))
                                {
                                    Console.ForegroundColor = ConsoleColor.Green;
                                    Console.WriteLine("\n✅ WALIDACJA: Wszystkie 3 fazy HITL zostały zatwierdzone.");
                                }
                                else
                                {
                                    Console.ForegroundColor = ConsoleColor.Red;
                                    Console.WriteLine("\n⚠️  WALIDACJA: Nie wszystkie fazy HITL zostały zatwierdzone!");
                                }
                                Console.ResetColor();
                            }

                            public static void PrintBanner()
                            {
                                Console.ForegroundColor = ConsoleColor.Cyan;
                                Console.WriteLine("╔════════════════════════════════════════════════════════════════╗");
                                Console.WriteLine("║  🚀 AI Content Team : v6c: Guaranteed HITL Pipeline          ║");
                                Console.WriteLine("╠════════════════════════════════════════════════════════════════╣");
                                Console.WriteLine("║  [1] Researcher                                               ║");
                                Console.WriteLine("║       ↓                                                       ║");
                                Console.WriteLine("║      PrepareResearch → ResearchPort → GateResearch  🛑 HITL#1 ║");
                                Console.WriteLine("║       ↓ fan-out                                               ║");
                                Console.WriteLine("║  [2a] Copywriter ══╗                        RÓWNOLEGLE ⚡     ║");
                                Console.WriteLine("║  [2b] Strateg    ══╝                                         ║");
                                Console.WriteLine("║       ↓ fan-in barrier → aggregator                           ║");
                                Console.WriteLine("║      PrepareEditorial → EditorialPort → GateEditorial 🛑 HITL#2║");
                                Console.WriteLine("║       ↓                                                       ║");
                                Console.WriteLine("║  [3] Redaktor (edycja, bez publikacji)                        ║");
                                Console.WriteLine("║       ↓                                                       ║");
                                Console.WriteLine("║      PreparePublish → PublishPort → GatePublish     🛑 HITL#3 ║");
                                Console.WriteLine("║       ↓                                                       ║");
                                Console.WriteLine("║  [4] PublishExecutor (publikacja po zatwierdzeniu)             ║");
                                Console.WriteLine("║                                                                ║");
                                Console.WriteLine("║  Wszystkie HITL via RequestPort : GWARANCJA zatrzymania       ║");
                                Console.WriteLine("║  Agent NIE MOŻE ominąć zatwierdzenia                          ║");
                                Console.WriteLine("╚════════════════════════════════════════════════════════════════╝");
                                Console.ResetColor();
                            }
                        }
                    
                

Działanie aplikacji

                    


                        [VIZ] Mermaid zapisany:  D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz\workflow_graph.mmd
                        [VIZ] DOT zapisany:      D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz\workflow_graph.dot
                        [VIZ] Wszystkie pliki w: D:\PanNiebieski\Webinary\MSTECH_MicrosoftAgentFrameworkMain\AgentDemo4\bin\Debug\net10.0\workflow_viz

                        ╔════════════════════════════════════════════════════════════════╗
                        ║  🚀 AI Content Team : v6c: Guaranteed HITL Pipeline          ║
                        ╠════════════════════════════════════════════════════════════════╣
                        ║  [1] Researcher                                               ║
                        ║       ↓                                                       ║
                        ║      PrepareResearch → ResearchPort → GateResearch  🛑 HITL#1 ║
                        ║       ↓ fan-out                                               ║
                        ║  [2a] Copywriter ══╗                        RÓWNOLEGLE ⚡     ║
                        ║  [2b] Strateg    ══╝                                         ║
                        ║       ↓ fan-in barrier → aggregator                           ║
                        ║      PrepareEditorial → EditorialPort → GateEditorial 🛑 HITL#2║
                        ║       ↓                                                       ║
                        ║  [3] Redaktor (edycja, bez publikacji)                        ║
                        ║       ↓                                                       ║
                        ║      PreparePublish → PublishPort → GatePublish     🛑 HITL#3 ║
                        ║       ↓                                                       ║
                        ║  [4] PublishExecutor (publikacja po zatwierdzeniu)             ║
                        ║                                                                ║
                        ║  Wszystkie HITL via RequestPort : GWARANCJA zatrzymania       ║
                        ║  Agent NIE MOŻE ominąć zatwierdzenia                          ║
                        ╚════════════════════════════════════════════════════════════════╝
                        ⏳ 🔍 Researcher: Pobieram transkrypcję z: https://youtube.com/watch?v=maf100demo
                        [MW:Log] ▶ Researcher start (1 wiadomości)
                        [MW:Audit] 🔧 Researcher → get_youtube_transcript()
                        [MW:Audit] ✓ get_youtube_transcript zakończony (25ms)
                        [MW:Log] ◀ Researcher zakończony (4842ms, 1037 znaków output)

                        ┌─────────────────────────────────────────────────────────────┐
                        │ 🔍 RESEARCH ZAKOŃCZONY : 5 tez wyodrębnionych
                        └─────────────────────────────────────────────────────────────┘
                        • **MAF 1.0 to fuzja Semantic Kernel + AutoGen** : nowe framework łączy obie technologie w jedno stabilne API z SLA produkcyjnym gotowym do deploymentu w systemach krytycznych
                        • **Trzy typy workflowów ze streamingiem** : sequential, concurrent i handoff workflows umożliwiają budowanie zaawansowanych scenariuszy agentowych z obsługą realtime data flow
                        • **MCP jako pierwszoklasowy obywatel** : Model Context Protocol jest w pełni zintegrowany z dynamicznym odkrywaniem i zarządzaniem tools, eliminując boilerplate
                        • **Human-in-the-Loop przez ApprovalRequiredAIFunction** : jeden prosty wrapper pozwala implementować approval workflows i audyt decyzji AI bez skomplikowanej architektury
                        • **Agent Skills z progressive disclosure** : wiedza dziedzinowa w formacie SKILL.md umożliwia stopniowe ujawnianie możliwości agentów, ułatwiając maintainability i governance
                        ⏱️  Faza 1 (Research): 0ms

                        ✔ Researcher zakończony
                        ⏳ 🛑 PrepareResearch: ⏸️  Czekam na zatwierdzenie fazy: Research

                        ✔ PrepareResearch zakończony

                        ╔══════════════════════════════════════════════════════════════╗
                        ║  🛑 WORKFLOW HITL : Zatwierdzenie fazy: Research          ║
                        ╚══════════════════════════════════════════════════════════════╝

                        📋 Podsumowanie:
                        URL: https://youtube.com/watch?v=maf100demo
                        Grupa docelowa: polscy .NET developerzy, seniorzy i architekci
                        Tezy (5):
                            • **MAF 1.0 to fuzja Semantic Kernel + AutoGen** : nowe framework łączy obie technologie w jedno stabilne API z SLA produkcyjnym gotowym do deploymentu w systemach krytycznych
                            • **Trzy typy workflowów ze streamingiem** : sequential, concurrent i handoff workflows umożliwiają budowanie zaawansowanych scenariuszy agentowych z obsługą realtime data flow
                            • **MCP jako pierwszoklasowy obywatel** : Model Context Protocol jest w pełni zintegrowany z dynamicznym odkrywaniem i zarządzaniem tools, eliminując boilerplate
                            • **Human-in-the-Loop przez ApprovalRequiredAIFunction** : jeden prosty wrapper pozwala implementować approval workflows i audyt decyzji AI bez skomplikowanej architektury
                            • **Agent Skills z progressive disclosure** : wiedza dziedzinowa w formacie SKILL.md umożliwia stopniowe ujawnianie możliwości agentów, ułatwiając maintainability i governance

                        👉 Zatwierdzić fazę 'Research'? (t/n, opcjonalnie feedback po spacji): t

                        ✅ Faza 'Research' zatwierdzona!

                        ✔ ResearchPort zakończony

                        ✔ ResearchPort zakończony

                        ✅ HITL Research: ZATWIERDZONO

                        ✔ GateResearch zakończony
                        ⏳ 📊 Strateg: Analizuję trendy i przygotowuję strategię...
                        [MW:Log] ▶ Strateg start (1 wiadomości)
                        [MW:Log] ▶ Copywriter start (1 wiadomości)
                        ⏳ ✍️  Copywriter: Generuję draft posta na LinkedIn...
                        [MW:Audit] 🔧 Copywriter → load_skill()
                        [MW:Audit] ✓ load_skill zakończony (3ms)
                        [MW:Audit] 🔧 Copywriter → read_skill_resource()
                        [MW:Audit] ✓ read_skill_resource zakończony (2ms)
                        [MW:Audit] 🔧 Strateg → _Main_g_GetLinkedInTrends_0_4()
                        [MW:Audit] ✓ _Main_g_GetLinkedInTrends_0_4 zakończony (0ms)
                        [MW:Audit] 🔧 Strateg → _Main_g_GetLinkedInTrends_0_4()
                        [MW:Audit] ✓ _Main_g_GetLinkedInTrends_0_4 zakończony (0ms)
                        [MW:Audit] 🔧 Strateg → load_skill()
                        [MW:Audit] ✓ load_skill zakończony (0ms)
                        [MW:Audit] 🔧 Strateg → load_skill()
                        [MW:Audit] ✓ load_skill zakończony (0ms)
                        [MW:Log] ◀ Copywriter zakończony (11259ms, 1065 znaków output)

                        ✓ Draft gotowy (1065 znaków)
                        ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
                        Teraz przeczytam branć voice, aby dostosować się do tonu:
                        Mam już wystarczające wytyczne. Napiszę post na podstawie tez i instrukcji z loadowanego skilla:

                        ---

                        Właśnie przestał być eksperymentem. 🎯...
                        ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

                        ✔ Copywriter zakończony
                        [MW:Log] ◀ Strateg zakończony (12541ms, 2089 znaków output)

                        ✓ Analiza strategiczna gotowa
                        Hook: "Pozwól, że najpierw sprawdzę aktualne trendy na LinkedIn w kategorii AI i Microsoft, a następnie załaduję niez"ędne umiejętności strategiczne.
                        Hashtagi: #dotnet #ai
                        Uwagi: Brak dodatkowych uwag.
                        ⏱️  Faza 2 (równoległa): 38634ms

                        ✔ Strateg zakończony

                        ✔ Aggregator zakończony

                        ✔ Aggregator zakończony
                        ⏳ 🛑 PrepareEditorial: ⏸️  Czekam na zatwierdzenie fazy: Editorial

                        ✔ PrepareEditorial zakończony

                        ╔══════════════════════════════════════════════════════════════╗
                        ║  🛑 WORKFLOW HITL : Zatwierdzenie fazy: Editorial         ║
                        ╚══════════════════════════════════════════════════════════════╝

                        📋 Podsumowanie:
                        DRAFT (1065 znaków):
                        Teraz przeczytam branć voice, aby dostosować się do tonu:
                        Mam już wystarczające wytyczne. Napiszę post na podstawie tez i instrukcji z loadowanego skilla:

                        ---

                        Właśnie przestał być eksperymentem. 🎯

                        MAF 1.0 to nie kolejny "nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.

                        Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.

                        Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować co agent wie i kiedy to wie : maintainability bez przebicia w głowę.

                        Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔

                        #dotnet #csharp #ai #agentframework #semantickernel

                        STRATEGIA:
                        Hook: Pozwól, że najpierw sprawdzę aktualne trendy na LinkedIn w kategorii AI i Microsoft, a następnie załaduję niezbędne umiejętności strategiczne.
                        Hashtagi: #dotnet #ai
                        Uwagi: Brak dodatkowych uwag.

                        👉 Zatwierdzić fazę 'Editorial'? (t/n, opcjonalnie feedback po spacji): t

                        ✅ Faza 'Editorial' zatwierdzona!

                        ✔ EditorialPort zakończony

                        ✔ EditorialPort zakończony

                        ✅ HITL Editorial: ZATWIERDZONO

                        ✔ GateEditorial zakończony
                        [MW:Log] ▶ Redaktor start (1 wiadomości)
                        ⏳ 📝 Redaktor: Scala draft i strategię, przygotowuje finalny post...
                        [MW:Log] ◀ Redaktor zakończony (1824ms, 905 znaków output)

                        ✔ Redaktor zakończony
                        ⏳ 🛑 PreparePublish: ⏸️  Czekam na zatwierdzenie fazy: Publish

                        ✔ PreparePublish zakończony

                        ╔══════════════════════════════════════════════════════════════╗
                        ║  🛑 WORKFLOW HITL : Zatwierdzenie fazy: Publish           ║
                        ╚══════════════════════════════════════════════════════════════╝

                        📋 Podsumowanie:
                        FINALNY POST (zredagowany):
                        Właśnie przestał być eksperymentem. 🎯

                        MAF 1.0 to nie kolejny „nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.

                        Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy, które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.

                        Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować, co agent wie i kiedy to wie : maintainability bez przebicia w głowę.

                        Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔

                        #dotnet #ai #csharp #agentframework #semantickernel

                        Hashtagi: #dotnet #ai
                        Zredagowany o: 11:18:44

                        👉 Zatwierdzić fazę 'Publish'? (t/n, opcjonalnie feedback po spacji): t

                        ✅ Faza 'Publish' zatwierdzona!

                        ✔ PublishPort zakończony

                        ✔ PublishPort zakończony

                        ✅ HITL Publish: ZATWIERDZONO

                        ✔ GatePublish zakończony

                        ✅ POST OPUBLIKOWANY NA LINKEDIN:
                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                        Właśnie przestał być eksperymentem. 🎯

                        MAF 1.0 to nie kolejny „nowoczesny framework" : to fuzja Semantic Kernel i AutoGen w jedno stabilne API z rzeczywistym SLA dla systemów krytycznych. Jeśli budowałeś agenty zaraz-po-zaraz fixując boilerplate, wiedz że się kończy.

                        Trzy typy workflowów (sequential, concurrent, handoff) pozwalają implementować rzeczy, które wcześniej wymagały custom choreografii. MCP jest w pełni zintegrowany : nie definiujesz tools, framework je odkrywa. Human-in-the-Loop przez ApprovalRequiredAIFunction to jeden wrapper zamiast całej architektury audytu.

                        Ale najciekawsza część? Agent Skills w formacie SKILL.md. Progressive disclosure wiedzy domenowej. Wreszcie umiem kontrolować, co agent wie i kiedy to wie : maintainability bez przebicia w głowę.

                        Tych zmian czekaliśmy. Czy już testowałeś MAF 1.0 w swoim projekcie? 🤔

                        #dotnet #ai #csharp #agentframework #semantickernel

                        #dotnet #ai
                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


                        ✔ Publish zakończony

                        ════════════════════════════════════════════════════════════════════
                        🏁 Pipeline zakończony! Łączny czas: 53471ms
                        ════════════════════════════════════════════════════════════════════

                        💡 Copywriter i Strateg startowali jednocześnie :
                        czas fazy 2 = max(t_copywriter, t_strateg)

                        ┌─────────────────────────────────────────────────────────┐
                        │  📋 AUDIT LOG : Historia zatwierdzeń HITL               │
                        ├─────────────────────────────────────────────────────────┤
                        │  ✅ Research              26072ms
                        │  ✅ Editorial              4047ms
                        │  ✅ Publish                4064ms
                        └─────────────────────────────────────────────────────────┘

                        ✅ WALIDACJA: Wszystkie 3 fazy HITL zostały zatwierdzone.

                        ┌─────────────────────────────────────────────────────────┐
                        │  📊 MIDDLEWARE METRICS : Raport pipeline'u              │
                        ├─────────────────────────────────────────────────────────┤
                        │  Agent Runs:                                            │
                        │    ✅ Researcher            4843ms │
                        │    ✅ Copywriter           11259ms │
                        │    ✅ Strateg              12541ms │
                        │    ✅ Redaktor              1824ms │
                        │                                                         │
                        │  Function Calls:                                        │
                        │    🔧 Researcher/get_youtube_transcript    25ms │
                        │    🔧 Copywriter/load_skill           4ms │
                        │    🔧 Copywriter/read_skill_resource     3ms │
                        │    🔧 Strateg/_Main_g_GetLinkedInTrends_0_4     1ms │
                        │    🔧 Strateg/_Main_g_GetLinkedInTrends_0_4     0ms │
                        │    🔧 Strateg/load_skill           0ms │
                        │    🔧 Strateg/load_skill           0ms │
                        │                                                         │
                        │  Tokeny (szacunkowo):                                   │
                        │    Input:  ~     889 tokenów               │
                        │    Output: ~    1273 tokenów               │
                        │    Razem:  ~    2162 tokenów               │
                        └─────────────────────────────────────────────────────────┘
                    
                
A może DevUI do testów i prototypowania?

AgentDevUIDemo/Program.cs

                    
                        // ============================================================================
                        // AgentDevUIDemo/Program.cs : DevUI hosting dla AiContentTeam workflow
                        // ============================================================================
                        //
                        // Dwa workflow:
                        //   content-pipeline-dev  : bez HITL, do DevUI (Development)
                        //   content-pipeline      : pełny HITL, do produkcji (opcjonalnie)
                        //
                        // Znane ograniczenia DevUI w .NET:
                        //   1. Entry point workflow musi być ChatClientAgent (nie custom Executor)
                        //   2. ApprovalStage (HITL) nie działa : DevUI nie ma UI do zatwierdzania
                        //
                        // Uruchomienie:
                        //   cd AgentDevUIDemo
                        //   dotnet run
                        //   # → http://localhost:5050/devui → wybierz "content-pipeline-dev"
                        //
                        // ============================================================================

                        using AiContentTeam;
                        using AiContentTeam.Guardrails;
                        using AiContentTeam.Middleware;
                        using Microsoft.Agents.AI;
                        using Microsoft.Agents.AI.DevUI;
                        using Microsoft.Agents.AI.Hosting;
                        using Microsoft.Agents.AI.Workflows;
                        using Microsoft.Extensions.AI;
                        using ModelContextProtocol.Client;

                        var builder = WebApplication.CreateBuilder(args);

                        // ── IChatClient ───────────────────────────────────────────────────────────────

                        IChatClient chatClient = new Anthropic.AnthropicClient()
                            .AsIChatClient("claude-haiku-4-5");

                        builder.Services.AddChatClient(chatClient);

                        // ── MCP tools ────────────────────────────────────────────────────────────────

                        builder.Services.AddSingleton<IList<McpClientTool>>(sp =>
                        {
                            return Task.Run(async () =>
                            {
                                var mcpClient = await McpClient.CreateAsync(
                                    new HttpClientTransport(new HttpClientTransportOptions
                                    {
                                        Endpoint = new Uri("http://localhost:2001/mcp")
                                    }));
                                return await mcpClient.ListToolsAsync();
                            }).GetAwaiter().GetResult();
                        });

                        // ── Skills provider ───────────────────────────────────────────────────────────

                        #pragma warning disable MAAI001
                        builder.Services.AddSingleton(
                            new AgentSkillsProvider(Path.Combine(AppContext.BaseDirectory, "skills")));
                        #pragma warning restore MAAI001

                        // ── Guardrails ────────────────────────────────────────────────────────────────

                        var contentPolicy = new ContentPolicyGuardrail();
                        var brandSafety = new BrandSafetyGuardrail(
                            competitors: ["CompetitorA", "CompetitorB"],
                            requireCTA: true);
                        var outputLength = new OutputLengthGuardrail(maxChars: 3000);

                        // ── Agenty w DI ───────────────────────────────────────────────────────────────

                        builder.Services.AddKeyedSingleton<AIAgent>("Researcher", (sp, _) =>
                        {
                            var chat = sp.GetRequiredService<IChatClient>();
                            var mcpTools = sp.GetRequiredService<IList<McpClientTool>>();

                            return GuardrailFactory.WrapWithFullPipeline(
                                new ChatClientAgent(chat, new ChatClientAgentOptions
                                {
                                    Name = "Researcher",
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś research agentem. Twoim JEDYNYM zadaniem jest:
                                            1. UŻYJ NARZĘDZIA GetYouTubeTranscript aby pobrać transkrypcję z podanego URL
                                            2. Na podstawie wyniku wyodrębnij dokładnie 5 kluczowych tez jako bullet pointy

                                            ZASADY:
                                            - ZAWSZE wywołuj narzędzie GetYouTubeTranscript
                                            - Odpowiadaj po polsku
                                            - Każdą tezę pisz w osobnej linii zaczynającej się od "• "
                                            """,
                                        Tools = [.. mcpTools]
                                    }
                                }),
                                enableLogging: true,
                                enableRetry: true);
                        });

                        builder.Services.AddKeyedSingleton<AIAgent>("Copywriter", (sp, _) =>
                        {
                            var chat = sp.GetRequiredService<IChatClient>();
                            var skills = sp.GetRequiredService<AgentSkillsProvider>();

                            return GuardrailFactory.WrapWithFullPipeline(
                                new ChatClientAgent(chat, new ChatClientAgentOptions
                                {
                                    Name = "Copywriter",
                                    AIContextProviders = [skills],
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś copywriterem specjalizującym się w treściach dla .NET developerów.
                                            Na podstawie tez od Researchera napisz post na LinkedIn.

                                            ZASADY:
                                            - 150-250 słów, max 3 emoji
                                            - Mocny hook w 1. zdaniu
                                            - Zakończ pytaniem do społeczności
                                            - Napisz TYLKO treść posta, bez meta-komentarza
                                            - Odpowiadaj po polsku
                                            """
                                    }
                                }),
                                enableLogging: true,
                                enableRetry: true,
                                contentPolicy, brandSafety, outputLength);
                        });

                        builder.Services.AddKeyedSingleton<AIAgent>("Strateg", (sp, _) =>
                        {
                            var chat = sp.GetRequiredService<IChatClient>();
                            var skills = sp.GetRequiredService<AgentSkillsProvider>();

                            return GuardrailFactory.WrapWithFullPipeline(
                                new ChatClientAgent(chat, new ChatClientAgentOptions
                                {
                                    Name = "Strateg",
                                    AIContextProviders = [skills],
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś strategiem social media dla społeczności .NET developerów.
                                            Przeanalizuj temat i odpowiedz STRUKTURALNIE:

                                            ANALIZA TRENDÓW: [2-3 zdania]
                                            REKOMENDOWANY HOOK: [jedno zdanie]
                                            SUGEROWANE HASHTAGI: [max 5, format #tag]
                                            UWAGI O ZAANGAŻOWANIU: [1-2 zdania]

                                            Odpowiadaj po polsku.
                                            """,
                                        Tools = [AIFunctionFactory.Create(GetLinkedInTrends)]
                                    }
                                }),
                                enableLogging: true,
                                enableRetry: true,
                                outputLength);
                        });

                        builder.Services.AddKeyedSingleton<AIAgent>("Redaktor", (sp, _) =>
                        {
                            var chat = sp.GetRequiredService<IChatClient>();

                            return GuardrailFactory.WrapWithFullPipeline(
                                new ChatClientAgent(chat, new ChatClientAgentOptions
                                {
                                    Name = "Redaktor",
                                    ChatOptions = new()
                                    {
                                        Instructions = """
                                            Jesteś redaktorem naczelnym. Dostałeś draft i analizę strategiczną.
                                            Zastosuj rekomendacje, popraw błędy językowe.
                                            Napisz TYLKO finalną treść posta po polsku.
                                            """
                                    }
                                }),
                                enableLogging: true,
                                enableRetry: true,
                                contentPolicy, brandSafety, outputLength);
                        });

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  content-pipeline-dev : BEZ HITL, do DevUI
                        // ══════════════════════════════════════════════════════════════════════════════

                        builder.AddWorkflow("content-pipeline-dev", (sp, key) =>
                        {
                            var chat = sp.GetRequiredService<IChatClient>();
                            var researcherAgent = sp.GetRequiredKeyedService<AIAgent>("Researcher");
                            var copywriterAgent = sp.GetRequiredKeyedService<AIAgent>("Copywriter");
                            var strategAgent = sp.GetRequiredKeyedService<AIAgent>("Strateg");
                            var redaktorAgent = sp.GetRequiredKeyedService<AIAgent>("Redaktor");

                            // Entry point: ChatClientAgent (DevUI wymaga tego)
                            var entryAgent = new ChatClientAgent(chat, new ChatClientAgentOptions
                            {
                                Name = "InputParser",
                                ChatOptions = new()
                                {
                                    Instructions = """
                                        Jesteś parserem inputu. Użytkownik poda URL YouTube lub temat.
                                        Wyodrębnij URL i powtórz go dokładnie.
                                        Jeśli nie ma URL, powtórz temat dosłownie.
                                        Nie dodawaj żadnych komentarzy ani wyjaśnień.
                                        Odpowiedz JEDNYM zdaniem lub samym URL-em.
                                        """
                                }
                            });

                            // Bridge: string → ResearchRequest (bo ResearcherExecutor oczekuje ResearchRequest)
                            var inputBridge = new InputBridgeExecutor();

                            // Executory
                            var researcher = new ResearcherExecutor(researcherAgent);
                            var copywriter = new CopywriterExecutor(copywriterAgent);
                            var strateg = new StrategExecutor(strategAgent);
                            var aggregator = new AggregatorExecutor();
                            var redaktor = new RedaktorExecutor(redaktorAgent);
                            var publisher = new PublishExecutor();

                            IEnumerable<ExecutorBinding> fanInSources = [copywriter, strateg];

                            // Graf: prosty, bez HITL
                            // W workflow zamiast:
                            //   entryAgent → inputBridge → researcher(executor)
                            // Daj:
                            //   entryAgent → researcherAgent(ChatClientAgent) → copywriter + strateg

                            return new WorkflowBuilder(entryAgent)
                                .WithName(key)
                                .AddEdge(entryAgent, researcherAgent)    // ← sam agent, nie executor
                                .AddFanOutEdge(researcherAgent, [copywriterAgent, strategAgent])
                                .AddFanInBarrierEdge(
                                    (IEnumerable<ExecutorBinding>)[copywriterAgent, strategAgent],
                                    redaktorAgent)
                                .WithOutputFrom(redaktorAgent)
                                .Build();
                        }).AddAsAIAgent();

                        // ── OpenAI + DevUI ────────────────────────────────────────────────────────────

                        builder.Services.AddOpenAIResponses();
                        builder.Services.AddOpenAIConversations();
                        builder.Logging.SetMinimumLevel(LogLevel.Debug);

                        // ── Build & Map ───────────────────────────────────────────────────────────────

                        var app = builder.Build();

                        // Middleware na wyjątki : PRZED endpointami
                        app.Use(async (context, next) =>
                        {
                            try
                            {
                                await next();
                            }
                            catch (Exception ex)
                            {
                                Console.ForegroundColor = ConsoleColor.Red;
                                Console.WriteLine($"[EXCEPTION] {ex}");
                                Console.ResetColor();
                                throw;
                            }
                        });

                        app.MapOpenAIResponses();
                        app.MapOpenAIConversations();

                        if (app.Environment.IsDevelopment())
                        {
                            app.MapDevUI();
                        }

                        app.MapGet("/health", () => Results.Ok(new
                        {
                            status = "healthy",
                            pipeline = "AiContentTeam : DevUI",
                            devui = "/devui",
                            features = new[] { "No HITL (dev)", "Middleware", "Guardrails", "Fan-out/Fan-in" }
                        }));

                        // ── Start ─────────────────────────────────────────────────────────────────────

                        app.Urls.Add("http://localhost:5050");

                        Console.ForegroundColor = ConsoleColor.Cyan;
                        Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
                        Console.WriteLine("║  🚀 AiContentTeam : DevUI Server                         ║");
                        Console.WriteLine("║                                                           ║");
                        Console.WriteLine("║  DevUI:       http://localhost:5050/devui                 ║");
                        Console.WriteLine("║  Health:      http://localhost:5050/health                ║");
                        Console.WriteLine("║                                                           ║");
                        Console.WriteLine("║  Workflow:    content-pipeline-dev (no HITL)              ║");
                        Console.WriteLine("║                                                           ║");
                        Console.WriteLine("║  Pipeline:                                                ║");
                        Console.WriteLine("║    InputParser → Bridge → Researcher                     ║");
                        Console.WriteLine("║    → Copywriter + Strateg (fan-out)                      ║");
                        Console.WriteLine("║    → Aggregator → Redaktor → Publisher                   ║");
                        Console.WriteLine("║                                                           ║");
                        Console.WriteLine("║  Middleware: Logging, Retry, Guardrails, Function Audit   ║");
                        Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
                        Console.ResetColor();

                        app.Run();

                        // ── Tool helper ───────────────────────────────────────────────────────────────

                        [System.ComponentModel.Description("Sprawdza popularne trendy na LinkedIn w kategorii tech/AI")]
                        static string GetLinkedInTrends(
                            [System.ComponentModel.Description("Kategoria, np. 'AI', '.NET'")] string category)
                        {
                            return $"""
                                Top trendy w kategorii "{category}" (symulacja):
                                1. Microsoft Agent Framework : wysoka aktywność
                                2. .NET 10 Preview : umiarkowane zaangażowanie
                                3. Multi-agent orchestration : rosnący trend
                                """;
                        }

                        // ══════════════════════════════════════════════════════════════════════════════
                        //  InputBridgeExecutor : konwertuje string z ChatClientAgent na ResearchRequest
                        // ══════════════════════════════════════════════════════════════════════════════

                        /// <summary>
                        /// Bridge między ChatClientAgent (zwraca string) a ResearcherExecutor (oczekuje ResearchRequest).
                        /// Potrzebny bo DevUI wymaga ChatClientAgent jako entry point workflow.
                        /// Produkcyjny Program.cs (konsolowy) nie potrzebuje tego : tam InputParserExecutor
                        /// jest entry pointem i bezpośrednio tworzy ResearchRequest.
                        /// </summary>
                        public partial class InputBridgeExecutor() : Executor<string, ResearchRequest>("InputBridge")
                        {
                            public override ValueTask<ResearchRequest> HandleAsync(
                                string message,
                                IWorkflowContext context,
                                CancellationToken cancellationToken = default)
                            {
                                Console.WriteLine($"[InputBridge] Parsing: '{message}'");

                                return ValueTask.FromResult(new ResearchRequest(
                                    YouTubeUrl: message.Trim(),
                                    TargetAudience: "polscy .NET developerzy, seniorzy i architekci"));
                            }
                        }
                    
                
screenshot
Dlaczego MAF > Semantic Kernel

Uproszczone API: brak obiektu Kernel

Aspekt Semantic Kernel Agent Framework
Centralny obiekt Kernel (ciężki orkiestrator) Brak — agenty i workflow bezpośrednio
Rejestracja narzędzi Plugin → Kernel → [KernelFunction] AIFunctionFactory.Create(metoda)
Abstrakcja modelu Własne konektory SK IChatClient z Microsoft.Extensions.AI
Workflow Brak natywnego (planner, ale bez grafu) Graph-based WorkflowBuilder z executorami
HITL Brak natywnego mechanizmu RequestPort z gwarancją zatrzymania
Multi-agent Eksperymentalne AgentGroupChat Natywne orkiestracje: Sequential, Concurrent, GroupChat, Handoff, Magentic
  1. Brak zależności od Kernel
  1. Executor jako czysta abstrakcja
  1. Middleware na 3 poziomach
  1. Graph-based workflows
  1. Dependency Injection agentów
  1. Streaming i eventy jako first-class:
  1. Natywny HITL
  1. Lepsze narzędzia (tools)
  1. Agent Skills
  1. Durable workflows
  1. Lepsza obserwowalność
  1. Mniej kodu
Kiedy SK nadal wygrywa (uczciwe porównanie)
  1. Kiedy SK nadal wygrywa (uczciwe porównanie)
Podsumowanie
  1. Podsumowanie 1
  1. Podsumowanie 2

Źródła i referencje

Microsoft : oficjalne blogi Link
Ogłoszenie GA 1.0 devblogs.microsoft.com/agent-framework/microsoft-agent-framework-version-1-0/
Building Blocks Part 3 devblogs.microsoft.com/dotnet/microsoft-agent-framework-building-blocks-for-ai-part-3/
Release Candidate blog devblogs.microsoft.com/foundry/microsoft-agent-framework-reaches-release-candidate/
AG-UI multi-agent demo devblogs.microsoft.com/agent-framework/ag-ui-multi-agent-workflow-demo/
Agent Skills blog devblogs.microsoft.com/agent-framework/give-your-agents-domain-expertise…
Durable Workflows blog devblogs.microsoft.com/dotnet/durable-workflows-in-microsoft-agent-framework/
SK → MAF migracja FAQ devblogs.microsoft.com/agent-framework/semantic-kernel-and-microsoft-agent-framework/
Multi-agent na App Service techcommunity.microsoft.com/…/build-multi-agent-ai-apps-on-azure-app-service…
Deep-dive MAF 1.0 techcommunity.microsoft.com/…/the-future-of-agentic-ai-inside-microsoft-agent-framework-1-0/
MAF + Foundry + MCP + Aspire developer.microsoft.com/blog/build-a-real-world-example-with-microsoft-agent-framework…

Źródła i referencje

Microsoft Learn : dokumentacja (1/2) Link
Przegląd frameworka learn.microsoft.com/en-us/agent-framework/overview/
Przegląd (pivot C#) learn.microsoft.com/en-us/agent-framework/overview/?pivots=programming-language-csharp
Workflows overview learn.microsoft.com/en-us/agent-framework/workflows/
Human-in-the-Loop docs learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop
HITL (pivot C#) learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop?pivots=…csharp
Edges w grafie workflow learn.microsoft.com/en-us/agent-framework/workflows/edges
WorkflowBuilder docs learn.microsoft.com/en-us/agent-framework/workflows/workflows
Agent Skills docs learn.microsoft.com/en-us/agent-framework/agents/skills
Tool approval / HITL learn.microsoft.com/en-us/agent-framework/agents/tools/tool-approval?pivots=…csharp
Agent Middleware docs learn.microsoft.com/en-us/agent-framework/user-guide/agents/agent-middleware

Źródła i referencje

Microsoft Learn : dokumentacja (2/2) Link
HITL w orkiestracjach learn.microsoft.com/…/workflows/orchestrations/human-in-the-loop
MCP tools w agencie learn.microsoft.com/…/model-context-protocol/using-mcp-tools
Middleware termination learn.microsoft.com/en-us/agent-framework/agents/middleware/termination
Dodawanie middleware learn.microsoft.com/en-us/agent-framework/journey/adding-middleware
Migracja z Semantic Kernel learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel/
AG-UI getting started C# learn.microsoft.com/…/integrations/ag-ui/getting-started?pivots=…csharp
AG-UI overview learn.microsoft.com/en-us/agent-framework/integrations/ag-ui/
DevUI docs learn.microsoft.com/en-us/agent-framework/devui/
Hosting / ASP.NET setup learn.microsoft.com/en-us/agent-framework/get-started/hosting
MAF hosting / deployment (DeepWiki) deepwiki.com/microsoft/agent-framework/5-deployment-and-hosting

Źródła i referencje

GitHub : repozytoria i kod (1/2) Link
Główne repo MAF github.com/microsoft/agent-framework
Repo z przykładami github.com/microsoft/Agent-Framework-Samples
Sample: Agents github.com/…/dotnet/samples/02-agents/Agents
Sample: AgentProviders github.com/…/dotnet/samples/02-agents/AgentProviders
Sample: Workflows github.com/…/dotnet/samples/03-workflows
Sample: Workflow Viz github.com/…/dotnet/samples/03-workflows/Visualization
Sample: DevUI setup github.com/…/dotnet/samples/GettingStarted/DevUI
Sample: DevUI Program.cs github.com/…/GettingStarted/DevUI/Program.cs
Sample: Declarative Agent github.com/…/agent-samples/chatclient/GetWeather.yaml
Sample: Multi-turn chat github.com/…/01-get-started/03_multi_turn/Program.cs

Źródła i referencje

GitHub : repozytoria i kod (2/2) Link
Sample: Agent z Memory github.com/…/01-get-started/04_memory/Program.cs
Sample: Middleware github.com/…/02-agents/Agents/Agent_Step11_Middleware
Sample: Tool Approvals github.com/…/02-agents/Agents/Agent_Step01…
Sample: Agent + MCP github.com/…/02-agents/ModelContextProtocol/Agent_MCP
Sample: Agent Skills github.com/…/02-agents/AgentSkills/Agent_Step01_BasicSkil
Sample: Agent Workflow github.com/…/03-workflows/_StartHere/03_AgentWorkflow
Sample: Custom Executors github.com/…/CustomAgentExecutors/Program.cs
Sample: A2A Server github.com/…/A2AClientServer/A2AServer/Program.cs
Sample: A2A Client github.com/…/A2AClientServer/A2AClient/Program.cs
Issue: DevUI + executory github.com/microsoft/agent-framework/issues/2084
Issue: WorkflowOutput bug github.com/microsoft/agent-framework/issues/2691
Dyskusja: DevUI workflow github.com/microsoft/agent-framework/discussions/2531
Gist: DevUI wzorzec gist.github.com/wullemsb/70fae615a805dbfadbc51e53ea739101

Źródła i referencje

NuGet : pakiety Link
Core agentów MAF nuget.org/packages/Microsoft.Agents.AI
Workflow engine MAF nuget.org/packages/Microsoft.Agents.AI.Workflows
Source generator Roslyn nuget.org/packages/Microsoft.Agents.AI.Workflows.Generators
ASP.NET hosting MAF nuget.org/packages/Microsoft.Agents.AI.Hosting
DevUI debugger paczka nuget.org/packages/Microsoft.Agents.AI.DevUI
AG-UI ASP.NET hosting nuget.org/packages/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore
Abstrakcja IChatClient nuget.org/packages/Microsoft.Extensions.AI
MEAI adapter do OpenAI nuget.org/packages/Microsoft.Extensions.AI.OpenAI
MCP SDK dla C# nuget.org/packages/ModelContextProtocol
Klient OpenAI SDK nuget.org/packages/OpenAI
Klient Ollama (lokalne LLM) nuget.org/packages/OllamaSharp
Klient Anthropic/Claude nuget.org/packages/Anthropic
Wizualizacja grafów DOT nuget.org/packages/Graphviz

Źródła i referencje

Artykuły zewnętrzne i narzędzia Link
HITL practical guide blog.gopenai.com/…/human-in-the-loop-approvals-in-microsoft-agent-framework…
MCP integration w C# devleader.ca/2026/03/04/mcp-tool-integration-in-microsoft-agent-framework-in-c
Tool Approval + HITL devleader.ca/2026/03/11/tool-approval-and-humanintheloop-in-microsoft-agent-framework
GA 1.0 coverage visualstudiomagazine.com/…/microsoft-ships-production-ready-agent-framework-1-0…
MCP C# SDK v1 release dotnetstudioai.com/news/mcp-csharp-sdk-v1-released-dotnet/
Fan-Out/Fan-In pattern linkedin.com/pulse/multiagent-orchestration-microsoft-agent-framework-maf…
AG-UI frontend tutorial copilotkit.ai/blog/build-a-frontend-for-your-microsoft-agent-framework-agents-with-ag-ui
AG-UI dokumentacja docs.ag-ui.com/introduction
AG-UI Interactive Dojo dojo.ag-ui.com/microsoft-agent-framework-dotnet/feature/agentic.chat
MCP C# SDK quickstart csharp.sdk.modelcontextprotocol.io/concepts/getting-started.html
.NET Agents newsletter dotnetagents.substack.com
Podgląd diagramów Mermaid mermaid.live