diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs
new file mode 100644
index 00000000..f5d39f0d
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using Microsoft.Identity.Client.Extensions.Msal;
+using Microsoft.Identity.Client;
+using Microsoft.Agents.CopilotStudio.Client;
+
+namespace CopilotStudioClientSample
+{
+ ///
+ /// This sample uses an HttpClientHandler to add an authentication token to the request.
+ /// This is used for the interactive authentication flow.
+ /// For more information on how to setup various authentication flows, see the Microsoft Identity documentation at https://aka.ms/msal.
+ ///
+ /// Direct To engine connection settings.
+ internal class AddTokenHandler(SampleConnectionSettings settings) : DelegatingHandler(new HttpClientHandler())
+ {
+ private static readonly string _keyChainServiceName = "copilot_studio_client_app";
+ private static readonly string _keyChainAccountName = "copilot_studio_client";
+
+ private async Task AuthenticateAsync(CancellationToken ct = default!)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+
+ // Gets the correct scope for connecting to Copilot Studio based on the settings provided.
+ string[] scopes = [CopilotClient.ScopeFromSettings(settings)];
+
+ // Setup a Public Client application for authentication.
+ IPublicClientApplication app = PublicClientApplicationBuilder.Create(settings.AppClientId)
+ .WithAuthority(AadAuthorityAudience.AzureAdMyOrg)
+ .WithTenantId(settings.TenantId)
+ .WithRedirectUri("http://localhost")
+ .Build();
+
+ string currentDir = Path.Combine(AppContext.BaseDirectory, "mcs_client_console");
+
+ if (!Directory.Exists(currentDir))
+ {
+ Directory.CreateDirectory(currentDir);
+ }
+
+ StorageCreationPropertiesBuilder storageProperties = new("TokenCache", currentDir);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ storageProperties.WithLinuxUnprotectedFile();
+ }
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ storageProperties.WithMacKeyChain(_keyChainServiceName, _keyChainAccountName);
+ }
+ MsalCacheHelper tokenCacheHelper = await MsalCacheHelper.CreateAsync(storageProperties.Build());
+ tokenCacheHelper.RegisterCache(app.UserTokenCache);
+
+ IAccount? account = (await app.GetAccountsAsync()).FirstOrDefault();
+
+ AuthenticationResult authResponse;
+ try
+ {
+ authResponse = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(ct);
+ //authResponse = await app.AcquireTokenInteractive(scopes).ExecuteAsync(ct);
+ }
+ catch (MsalUiRequiredException)
+ {
+ authResponse = await app.AcquireTokenInteractive(scopes).ExecuteAsync(ct);
+ }
+ return authResponse;
+ }
+
+ ///
+ /// Handles sending the request and adding the token to the request.
+ ///
+ /// Request to be sent
+ ///
+ ///
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request.Headers.Authorization is null)
+ {
+ AuthenticationResult authResponse = await AuthenticateAsync(cancellationToken);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResponse.AccessToken);
+ }
+ return await base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs
new file mode 100644
index 00000000..652ff085
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.CopilotStudio.Client;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
+using Microsoft.Identity.Client.Extensions.Msal;
+using Microsoft.Identity.Client;
+
+namespace CopilotStudioClientSample
+{
+ ///
+ /// This sample uses an HttpClientHandler to add an authentication token to the request.
+ /// In this case its using client secret for the request.
+ ///
+ /// Direct To engine connection settings.
+ internal class AddTokenHandlerS2S(SampleConnectionSettings settings) : DelegatingHandler(new HttpClientHandler())
+ {
+ private static readonly string _keyChainServiceName = "copilot_studio_client_app";
+ private static readonly string _keyChainAccountName = "copilot_studio_client";
+
+ private IConfidentialClientApplication? _confidentialClientApplication;
+ private string[]? _scopes;
+
+ private async Task AuthenticateAsync(CancellationToken ct = default!)
+ {
+ if (_confidentialClientApplication == null)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+ _scopes = [CopilotClient.ScopeFromSettings(settings)];
+ _confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settings.AppClientId)
+ .WithAuthority(AzureCloudInstance.AzurePublic, settings.TenantId)
+ .WithClientSecret(settings.AppClientSecret)
+ .Build();
+
+ string currentDir = Path.Combine(AppContext.BaseDirectory, "mcs_client_console");
+
+ if (!Directory.Exists(currentDir))
+ {
+ Directory.CreateDirectory(currentDir);
+ }
+
+ StorageCreationPropertiesBuilder storageProperties = new("AppTokenCache", currentDir);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ storageProperties.WithLinuxUnprotectedFile();
+ }
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ storageProperties.WithMacKeyChain(_keyChainServiceName, _keyChainAccountName);
+ }
+ MsalCacheHelper tokenCacheHelper = await MsalCacheHelper.CreateAsync(storageProperties.Build());
+ tokenCacheHelper.RegisterCache(_confidentialClientApplication.AppTokenCache);
+ }
+
+ AuthenticationResult authResponse;
+ authResponse = await _confidentialClientApplication.AcquireTokenForClient(_scopes).ExecuteAsync(ct);
+ return authResponse;
+ }
+
+ ///
+ /// Handles sending the request and adding the token to the request.
+ ///
+ /// Request to be sent
+ ///
+ ///
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request.Headers.Authorization is null)
+ {
+ AuthenticationResult authResponse = await AuthenticateAsync(cancellationToken);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResponse.AccessToken);
+ }
+ return await base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs b/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs
new file mode 100644
index 00000000..b07fdeb7
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.Core.Models;
+using Microsoft.Agents.CopilotStudio.Client;
+
+namespace CopilotStudioClientSample;
+
+///
+/// This class is responsible for handling the Chat Console service and managing the conversation between the user and the Copilot Studio hosted Agent.
+///
+/// Connection Settings for connecting to Copilot Studio
+internal class ChatConsoleService(CopilotClient copilotClient) : IHostedService
+{
+ ///
+ /// This is the main thread loop that manages the back and forth communication with the Copilot Studio Agent.
+ ///
+ ///
+ ///
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
+ Console.Write("\nagent> ");
+
+ // Attempt to connect to the copilot studio hosted agent here
+ // if successful, this will loop though all events that the Copilot Studio agent sends to the client setup the conversation.
+ await foreach (Activity act in copilotClient.StartConversationAsync(emitStartConversationEvent:true, cancellationToken:cancellationToken))
+ {
+ System.Diagnostics.Trace.WriteLine($">>>>MessageLoop Duration: {sw.Elapsed.ToDurationString()}");
+ sw.Restart();
+ if (act is null)
+ {
+ throw new InvalidOperationException("Activity is null");
+ }
+ // for each response, report to the UX
+ PrintActivity(act);
+ }
+
+ // Once we are connected and have initiated the conversation, begin the message loop with the Console.
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Console.Write("\nuser> ");
+ string question = Console.ReadLine()!; // Get user input from the console to send.
+ Console.Write("\nagent> ");
+ // Send the user input to the Copilot Studio agent and await the response.
+ // In this case we are not sending a conversation ID, as the agent is already connected by "StartConversationAsync", a conversation ID is persisted by the underlying client.
+ sw.Restart();
+ await foreach (Activity act in copilotClient.AskQuestionAsync(question, null, cancellationToken))
+ {
+ System.Diagnostics.Trace.WriteLine($">>>>MessageLoop Duration: {sw.Elapsed.ToDurationString()}");
+ // for each response, report to the UX
+ PrintActivity(act);
+ sw.Restart();
+ }
+ }
+ sw.Stop();
+ }
+
+ ///
+ /// This method is responsible for writing formatted data to the console.
+ /// This method does not handle all of the possible activity types and formats, it is focused on just a few common types.
+ ///
+ ///
+ static void PrintActivity(IActivity act)
+ {
+ switch (act.Type)
+ {
+ case "message":
+ if (act.TextFormat == "markdown")
+ {
+ Console.WriteLine(act.Text);
+ if (act.SuggestedActions?.Actions.Count > 0)
+ {
+ Console.WriteLine("Suggested actions:\n");
+ act.SuggestedActions.Actions.ToList().ForEach(action => Console.WriteLine("\t" + action.Text));
+ }
+ }
+ else
+ {
+ Console.Write($"\n{act.Text}\n");
+ }
+ break;
+ case "typing":
+ Console.Write(".");
+ break;
+ case "event":
+ Console.Write("+");
+ break;
+ default:
+ Console.Write($"[{act.Type}]");
+ break;
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ System.Diagnostics.Trace.TraceInformation("Stopping");
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj b/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj
new file mode 100644
index 00000000..3d9354d8
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs b/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs
new file mode 100644
index 00000000..a882728d
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.Core.Models;
+using Microsoft.Agents.CopilotStudio.Client;
+
+namespace CopilotStudioClientSample;
+
+///
+/// This class manages communication between two Copilot Studio agents.
+/// It allows sending messages from Copilot 1 to Copilot 2 and provides an interactive chat interface.
+///
+internal class DualCopilotChatService : IHostedService
+{
+ private readonly IServiceProvider _serviceProvider;
+ private CopilotClient _copilot1Client;
+ private CopilotClient _copilot2Client;
+
+ public DualCopilotChatService(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ _copilot1Client = _serviceProvider.GetRequiredKeyedService("copilot1");
+ _copilot2Client = _serviceProvider.GetRequiredKeyedService("copilot2");
+ }
+
+ ///
+ /// Main service loop that provides interactive chat with both copilots
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ Console.WriteLine("=== Dual Copilot Chat Service Started ===");
+ Console.WriteLine("Commands:");
+ Console.WriteLine(" 1: - Send message to Copilot 1");
+ Console.WriteLine(" 2: - Send message to Copilot 2");
+ Console.WriteLine(" relay: - Send to Copilot 1, then relay response to Copilot 2");
+ Console.WriteLine(" quit - Exit");
+ Console.WriteLine("==========================================");
+
+ // Initialize both copilots
+ await InitializeCopilot(_copilot1Client, "Copilot 1", cancellationToken);
+ await InitializeCopilot(_copilot2Client, "Copilot 2", cancellationToken);
+
+ // Start interactive chat loop
+ await InteractiveChatLoop(cancellationToken);
+ }
+
+ ///
+ /// Initialize a copilot client and start its conversation
+ ///
+ private async Task InitializeCopilot(CopilotClient copilotClient, string name, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"\n[{name}] Initializing...");
+
+ await foreach (Activity act in copilotClient.StartConversationAsync(emitStartConversationEvent: true, cancellationToken: cancellationToken))
+ {
+ if (act is null) continue;
+ Console.WriteLine($"[{name}] {GetActivityText(act)}");
+ }
+
+ Console.WriteLine($"[{name}] Ready!");
+ }
+
+ ///
+ /// Interactive chat loop handling user commands
+ ///
+ private async Task InteractiveChatLoop(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Console.Write("\nCommand> ");
+ string input = Console.ReadLine()!;
+
+ if (string.IsNullOrWhiteSpace(input)) continue;
+ if (input.ToLower() == "quit") break;
+
+ try
+ {
+ await ProcessCommand(input, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Process user commands for copilot interactions
+ ///
+ private async Task ProcessCommand(string input, CancellationToken cancellationToken)
+ {
+ if (input.StartsWith("1:"))
+ {
+ string message = input.Substring(2).Trim();
+ await SendToCopilot(_copilot1Client, "Copilot 1", message, cancellationToken);
+ }
+ else if (input.StartsWith("2:"))
+ {
+ string message = input.Substring(2).Trim();
+ await SendToCopilot(_copilot2Client, "Copilot 2", message, cancellationToken);
+ }
+ else if (input.StartsWith("relay:"))
+ {
+ string message = input.Substring(6).Trim();
+ await RelayMessage(message, cancellationToken);
+ }
+ else
+ {
+ Console.WriteLine("[ERROR] Invalid command. Use 1:, 2:, relay:, or quit");
+ }
+ }
+
+ ///
+ /// Send a message to a specific copilot
+ ///
+ private async Task SendToCopilot(CopilotClient copilotClient, string name, string message, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"\n[YOU β {name}] {message}");
+ Console.Write($"[{name}] ");
+
+ await foreach (Activity act in copilotClient.AskQuestionAsync(message, null, cancellationToken))
+ {
+ if (act is null) continue;
+ Console.Write(GetActivityText(act));
+ }
+ Console.WriteLine(); // New line after response
+ }
+
+ ///
+ /// Relay a message from Copilot 1 to Copilot 2
+ /// This demonstrates inter-copilot communication
+ ///
+ private async Task RelayMessage(string originalMessage, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"\n[RELAY] Starting relay: {originalMessage}");
+
+ // Step 1: Send to Copilot 1
+ Console.WriteLine($"[YOU β Copilot 1] {originalMessage}");
+ Console.Write("[Copilot 1] ");
+
+ string copilot1Response = "";
+ await foreach (Activity act in _copilot1Client.AskQuestionAsync(originalMessage, null, cancellationToken))
+ {
+ if (act is null) continue;
+ string actText = GetActivityText(act);
+ Console.Write(actText);
+
+ if (act.Type == "message" && !string.IsNullOrEmpty(act.Text))
+ {
+ copilot1Response += act.Text + " ";
+ }
+ }
+ Console.WriteLine(); // New line
+
+ if (string.IsNullOrWhiteSpace(copilot1Response))
+ {
+ Console.WriteLine("[ERROR] No response from Copilot 1 to relay");
+ return;
+ }
+
+ // Step 2: Relay Copilot 1's response to Copilot 2
+ string relayMessage = $"Copilot 1 said: {copilot1Response.Trim()}";
+ Console.WriteLine($"\n[Copilot 1 β Copilot 2] {relayMessage}");
+ Console.Write("[Copilot 2] ");
+
+ await foreach (Activity act in _copilot2Client.AskQuestionAsync(relayMessage, null, cancellationToken))
+ {
+ if (act is null) continue;
+ Console.Write(GetActivityText(act));
+ }
+ Console.WriteLine(); // New line
+
+ Console.WriteLine("[RELAY] Complete!");
+ }
+
+ ///
+ /// Extract readable text from an activity
+ ///
+ private static string GetActivityText(IActivity act)
+ {
+ return act.Type switch
+ {
+ "message" when act.TextFormat == "markdown" => act.Text ?? "",
+ "message" => act.Text ?? "",
+ "typing" => ".",
+ "event" => "+",
+ _ => $"[{act.Type}]"
+ };
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ Console.WriteLine("Dual Copilot Chat Service stopped.");
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md b/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md
new file mode 100644
index 00000000..bfe5aac6
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md
@@ -0,0 +1,290 @@
+# Copilot Studio Client Console Sample
+
+This sample demonstrates how to use the `Microsoft.Agents.CopilotStudio.Client` package to create a console application that connects to and communicates with agents hosted in Microsoft Copilot Studio in DIFFERENT environments. This sample is for environments in same tenant, but can be extended across.
+This builds on original sample of copilotstudio-client, so make sure you have followed that prior to coming to this step.
+
+## ποΈ Architecture Overview
+
+```
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β Console App β β Azure AD β β Copilot Studio β
+β β β β β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β βAddTokenHdlr βββΌβββββΌβΊβ Auth β β β β Agent 1 β β
+β βββββββββββββββ β β β Service β β β βββββββββββββββ β
+β β β βββββββββββββββ β β β
+β βββββββββββββββ β βββββββββββββββββββ β βββββββββββββββ β
+β βCopilotClientβββΌββββββββββββββββββββββββββββββΌβΊβ Agent 2 β β
+β βββββββββββββββ β β βββββββββββββββ β
+β β β β
+βββββββββββββββββββ βββββββββββββββββββ
+```
+
+## π Prerequisites
+
+Before running this sample, you'll need:
+
+1. **Microsoft Copilot Studio access** with permissions to create and publish agents
+2. **Azure AD tenant** with permissions to create app registrations
+3. **.NET 8.0 SDK** installed on your development machine
+4. **Visual Studio Code** or **Visual Studio** for debugging
+
+## π€ Step 1: Create Agents in Copilot Studio
+
+### Create Your First Agent
+1. Navigate to [Microsoft Copilot Studio](https://copilotstudio.microsoft.com/)
+2. Click **"Create a copilot"** or **"New copilot"**
+3. Configure your agent:
+ - **Name**: `TestAgent` (or your preferred name)
+ - **Description**: Brief description of your agent's purpose
+ - **Language**: Select your preferred language
+4. **Publish your agent**:
+ - Click **"Publish"** in the top navigation
+ - Follow the publishing wizard to make your agent available
+
+### Create Your Second Agent (Optional)
+Repeat the above steps to create a second agent for testing multiple agent configurations.
+
+### Collect Agent Information
+For each agent you created:
+
+1. In Copilot Studio, navigate to **Settings** β **Advanced** β **Metadata**
+2. **Copy and save** the following values (you'll need them for configuration):
+ - **Schema Name**: (e.g., `cr73a_testAgent`)
+ - **Environment ID**: (e.g., `b4ccc464-f5b7-e266-bada-06122c419e03`)
+
+## π Step 2: Create Azure AD App Registration
+
+### Create the App Registration
+1. Open the [Azure Portal](https://portal.azure.com/)
+2. Navigate to **Azure Active Directory** (or **Microsoft Entra ID**)
+3. Go to **App registrations** β **New registration**
+4. Configure the registration:
+ - **Name**: `Copilot Studio Client Sample`
+ - **Supported account types**: `Accounts in this organizational directory only`
+ - **Redirect URI**:
+ - Platform: `Public client/native (mobile & desktop)`
+ - URI: `http://localhost`
+5. Click **Register**
+
+### Configure API Permissions
+1. In your newly created app registration, go to **API permissions**
+2. Click **Add a permission**
+3. Select **APIs my organization uses** tab
+4. Search for and select **"Power Platform API"**
+ > β οΈ **Note**: If you don't see "Power Platform API", follow [these instructions](https://learn.microsoft.com/power-platform/admin/programmability-authentication-v2#step-2-configure-api-permissions) to add it to your tenant first.
+5. Select **Delegated permissions**
+6. Expand **CopilotStudio** and check **`CopilotStudio.Copilots.Invoke`**
+7. Click **Add permissions**
+8. **(Recommended)** Click **Grant admin consent** for your organization
+
+### Collect App Registration Information
+From the **Overview** page of your app registration, copy and save:
+- **Application (client) ID**: (e.g., `525dc2dd-a78d-4770-9c21-71bea65fab56`)
+- **Directory (tenant) ID**: (e.g., `391bca38-63d1-4cbd-ba78-c74369b91888`)
+
+## βοΈ Step 3: Configure the Application Settings
+
+### Update appsettings.json
+1. Open `appsettings.json` in the project root
+2. Update the configuration sections with the values you collected:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "CopilotStudioClientSettings1": {
+ "DirectConnectUrl": "",
+ "EnvironmentId": "YOUR_ENVIRONMENT_ID_HERE",
+ "SchemaName": "YOUR_SCHEMA_NAME_HERE",
+ "TenantId": "YOUR_TENANT_ID_HERE",
+ "UseS2SConnection": false,
+ "AppClientId": "YOUR_APP_CLIENT_ID_HERE",
+ "AppClientSecret": ""
+ },
+ "CopilotStudioClientSettings2": {
+ "DirectConnectUrl": "",
+ "EnvironmentId": "YOUR_SECOND_ENVIRONMENT_ID_HERE",
+ "SchemaName": "YOUR_SECOND_SCHEMA_NAME_HERE",
+ "TenantId": "YOUR_TENANT_ID_HERE",
+ "UseS2SConnection": false,
+ "AppClientId": "YOUR_APP_CLIENT_ID_HERE",
+ "AppClientSecret": ""
+ }
+}
+```
+
+### Configuration Reference
+| Setting | Description | Example Value |
+|---------|-------------|---------------|
+| `EnvironmentId` | Environment ID from Copilot Studio | `b4ccc464-f5b7-e266-bada-06122c419e03` |
+| `SchemaName` | Schema name from Copilot Studio | `cr73a_testAgent` |
+| `TenantId` | Directory (tenant) ID from Azure AD | `391bca38-63d1-4cbd-ba78-c74369b91888` |
+| `AppClientId` | Application (client) ID from Azure AD | `525dc2dd-a78d-4770-9c21-71bea65fab56` |
+| `UseS2SConnection` | Set to `false` for interactive authentication | `false` |
+| `AppClientSecret` | Leave empty for public client apps | `""` |
+
+## πββοΈ Step 4: Build and Run the Sample
+
+### Method 1: Using .NET CLI
+```bash
+# Navigate to the project directory
+cd [your-project-directory]
+
+# Restore dependencies
+dotnet restore
+
+# Build the project
+dotnet build
+
+# Run the application
+dotnet run
+```
+
+### Method 2: Using Visual Studio Code
+1. Open the project folder in VS Code
+2. Press `F5` or go to **Run** β **Start Debugging**
+3. Select the **"Debug Program.cs"** configuration
+
+### Method 3: Using Visual Studio
+1. Open `CopilotStudioClient.csproj` in Visual Studio
+2. Press `F5` or click **Debug** β **Start Debugging**
+
+## π Authentication Flow
+
+### Interactive Authentication Process
+1. **First Run**: The application will open your default web browser
+2. **Sign In**: Enter your Microsoft credentials
+3. **Consent**: Grant permissions to access Copilot Studio on your behalf
+4. **Token Caching**: Subsequent runs will use cached tokens (stored in `mcs_client_console` folder)
+
+### Authentication Components
+- **`AddTokenHandler.cs`**: Handles interactive user authentication using MSAL
+- **`AddTokenHandlerS2S.cs`**: Handles service-to-service authentication (when `UseS2SConnection` is `true`)
+- **Token Cache**: Stored locally for improved performance on subsequent runs
+
+## π¬ Using the Console Interface
+
+### Console Commands and Interactions
+Once the application starts successfully, you'll see:
+
+```
+agent> [Initial agent response/greeting]
+
+user> [Type your message here]
+```
+
+### Console Interface Guide
+| Prompt | Description |
+|--------|-------------|
+| `user>` | Your input prompt - type your questions or commands here |
+| `agent>` | Agent responses from Copilot Studio |
+| `.` | Indicates the agent is typing |
+| `+` | Indicates system events (connection, processing, etc.) |
+| `[message]` | System activity type indicators |
+
+### Example Interaction
+```
+agent> Hello! I'm your Copilot Studio agent. How can I help you today?
+
+user> What's the weather like?
+
+agent> I'd be happy to help you with weather information. However, I'll need to know your location first. Could you please tell me which city or area you're interested in?
+
+user> Seattle, WA
+
+agent> [Agent provides weather information for Seattle]
+```
+
+## π§ Troubleshooting
+
+### Common Issues and Solutions
+
+#### 1. Breakpoints Not Being Hit
+**Problem**: Debugging breakpoints in `AddTokenHandler.cs` are not being triggered.
+
+**Solutions**:
+- **Check Authentication Cache**: Delete the `mcs_client_console` folder in your application's base directory to force re-authentication
+- **Verify Configuration**: Ensure `UseS2SConnection` is set to `false` in `appsettings.json`
+- **Check Build Configuration**: Ensure you're running in Debug mode, not Release
+- **Verify Token Flow**: The `AddTokenHandler` is only called when HTTP requests are made to Copilot Studio
+
+#### 2. Authentication Errors
+**Problem**: "Failed to authenticate" or login window doesn't appear.
+
+**Solutions**:
+- Verify your `AppClientId` and `TenantId` are correct
+- Ensure the redirect URI in Azure AD is exactly `http://localhost` (not https)
+- Check that the app registration has the correct API permissions
+- Try running the application as Administrator
+
+#### 3. Agent Connection Issues
+**Problem**: Cannot connect to the Copilot Studio agent.
+
+**Solutions**:
+- Verify the `EnvironmentId` and `SchemaName` are correct
+- Ensure your agent is published in Copilot Studio
+- Check that you have access to the environment where the agent is hosted
+- Verify your Azure AD user has permissions to invoke the agent
+
+#### 4. Token Cache Issues
+**Problem**: Authentication fails after it previously worked.
+
+**Solutions**:
+- Delete the token cache directory: `{AppContext.BaseDirectory}/mcs_client_console`
+- Clear browser cookies for Microsoft login pages
+- Try authenticating in an incognito/private browser window
+
+### Debug Settings
+To enable more detailed logging, update `appsettings.json`:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.Identity.Client": "Debug"
+ }
+ }
+}
+```
+
+## π Project Structure
+
+```
+CopilotStudioClient/
+βββ Program.cs # Application entry point and DI configuration
+βββ ChatConsoleService.cs # Main chat loop and console interface
+βββ AddTokenHandler.cs # Interactive authentication handler
+βββ AddTokenHandlerS2S.cs # Service-to-service authentication handler
+βββ SampleConnectionSettings.cs # Configuration model
+βββ TimeSpanExtensions.cs # Utility extensions
+βββ appsettings.json # Application configuration
+βββ CopilotStudioClient.csproj # Project file
+βββ README.md # Documentation
+```
+
+## π Additional Resources
+
+- [Microsoft Copilot Studio Documentation](https://docs.microsoft.com/power-virtual-agents/)
+- [Power Platform API Authentication](https://learn.microsoft.com/power-platform/admin/programmability-authentication-v2)
+- [Microsoft Identity Platform Documentation](https://docs.microsoft.com/azure/active-directory/develop/)
+- [MSAL.NET Documentation](https://docs.microsoft.com/azure/active-directory/develop/msal-net-overview)
+
+## π€ Support
+
+If you encounter issues with this sample:
+
+1. Check the [Troubleshooting](#-troubleshooting) section above
+2. Review the console output for detailed error messages
+3. Enable debug logging for more detailed information
+4. Ensure all prerequisites and configuration steps have been completed correctly
+
+---
+
+**Happy Chatting with your Copilot Studio Agents! π**
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs b/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs
new file mode 100644
index 00000000..d899b120
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using CopilotStudioClientSample;
+using Microsoft.Agents.CopilotStudio.Client;
+
+// Setup the Direct To Engine client example.
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+
+// Get the configuration settings for both copilots from the appsettings.json file.
+SampleConnectionSettings settings1 = new SampleConnectionSettings(builder.Configuration.GetSection("CopilotStudioClientSettings1"));
+SampleConnectionSettings settings2 = new SampleConnectionSettings(builder.Configuration.GetSection("CopilotStudioClientSettings2"));
+
+// Create http clients for both copilots
+builder.Services.AddHttpClient("copilot1").ConfigurePrimaryHttpMessageHandler(() =>
+{
+ if (settings1.UseS2SConnection)
+ {
+ return new AddTokenHandlerS2S(settings1);
+ }
+ else
+ {
+ return new AddTokenHandler(settings1);
+ }
+});
+
+builder.Services.AddHttpClient("copilot2").ConfigurePrimaryHttpMessageHandler(() =>
+{
+ if (settings2.UseS2SConnection)
+ {
+ return new AddTokenHandlerS2S(settings2);
+ }
+ else
+ {
+ return new AddTokenHandler(settings2);
+ }
+});
+
+// add Settings and instances of both Copilot Clients to the Current services.
+builder.Services
+ .AddSingleton(settings1)
+ .AddSingleton(settings2)
+ .AddKeyedTransient("copilot1", (s, key) =>
+ {
+ var logger = s.GetRequiredService().CreateLogger();
+ return new CopilotClient(settings1, s.GetRequiredService(), logger, "copilot1");
+ })
+ .AddKeyedTransient("copilot2", (s, key) =>
+ {
+ var logger = s.GetRequiredService().CreateLogger();
+ return new CopilotClient(settings2, s.GetRequiredService(), logger, "copilot2");
+ })
+ .AddHostedService();
+IHost host = builder.Build();
+host.Run();
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json b/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json
new file mode 100644
index 00000000..d1bd1db8
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "mcs.console": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DirectToEngineSettings__TenantId": "",
+ "DirectToEngineSettings__EnvironmentId": "",
+ "DirectToEngineSettings__AppClientId": "",
+ "DirectToEngineSettings__BotIdentifier": ""
+ }
+ }
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs b/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs
new file mode 100644
index 00000000..b5f9a8ec
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.CopilotStudio.Client;
+
+namespace CopilotStudioClientSample
+{
+ ///
+ /// Connection Settings extension for the sample to include appID and TenantId for creating authentication token.
+ ///
+ internal class SampleConnectionSettings : ConnectionSettings
+ {
+ ///
+ /// Use S2S connection for authentication.
+ ///
+ public bool UseS2SConnection { get; set; } = false;
+
+ ///
+ /// Tenant ID for creating the authentication for the connection
+ ///
+ public string? TenantId { get; set; }
+ ///
+ /// Application ID for creating the authentication for the connection
+ ///
+ public string? AppClientId { get; set; }
+
+ ///
+ /// Application secret for creating the authentication for the connection
+ ///
+ public string? AppClientSecret { get; set; }
+
+ ///
+ /// Create ConnectionSettings from a configuration section.
+ ///
+ ///
+ ///
+ public SampleConnectionSettings(IConfigurationSection config) :base (config)
+ {
+ AppClientId = config[nameof(AppClientId)] ?? throw new ArgumentException($"{nameof(AppClientId)} not found in config");
+ TenantId = config[nameof(TenantId)] ?? throw new ArgumentException($"{nameof(TenantId)} not found in config");
+ UseS2SConnection = config.GetValue(nameof(UseS2SConnection), false);
+ AppClientSecret = config[nameof(AppClientSecret)];
+ }
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs b/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs
new file mode 100644
index 00000000..2e784ac7
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs
@@ -0,0 +1,15 @@
+namespace CopilotStudioClientSample
+{
+ internal static class TimeSpanExtensions
+ {
+ ///
+ /// Returns a duration in the format hh:mm:ss:fff
+ ///
+ ///
+ ///
+ internal static string ToDurationString(this TimeSpan timeSpan)
+ {
+ return timeSpan.ToString(@"hh\:mm\:ss\.fff");
+ }
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json b/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json
new file mode 100644
index 00000000..aa2057b1
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json
@@ -0,0 +1,26 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Error",
+ "Microsoft.Hosting.Lifetime": "Warning"
+ }
+ },
+ "CopilotStudioClientSettings1": {
+ "DirectConnectUrl": "",
+ "EnvironmentId": "b4ccc464-f5b7-e266-bada-06122c419e03",
+ "SchemaName": "cr73a_testAgent",
+ "TenantId": "391bca38-63d1-4cbd-ba78-c74369b91888",
+ "UseS2SConnection": false,
+ "AppClientId": "525dc2dd-a78d-4770-9c21-71bea65fab56",
+ "AppClientSecret": ""
+ },
+ "CopilotStudioClientSettings2": {
+ "DirectConnectUrl": "",
+ "EnvironmentId": "a865c82b-3806-efa9-a5a3-df5889dfa013",
+ "SchemaName": "cr082_agent2",
+ "TenantId": "391bca38-63d1-4cbd-ba78-c74369b91888",
+ "UseS2SConnection": false,
+ "AppClientId": "525dc2dd-a78d-4770-9c21-71bea65fab56",
+ "AppClientSecret": ""
+ }
+}
diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln b/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln
new file mode 100644
index 00000000..38fdd958
--- /dev/null
+++ b/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopilotStudioClient", "CopilotStudioClient.csproj", "{207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8713DD82-C2D9-41B9-B2D7-480AB84702E5}
+ EndGlobalSection
+EndGlobal