Agent Framework (Simple) Sample

Overview

This is a simple sample showing how to use the Agent Framework as the orchestrator in an agent using the Microsoft Agent 365 SDK and Microsoft 365 Agents SDK It covers:

  • Observability: End-to-end tracing, caching, and monitoring for agent applications
  • Notifications: Services and models for managing user notifications
  • Tools: Model Context Protocol tools for building advanced agent solutions
  • Hosting Patterns: Hosting with Microsoft 365 Agents SDK

This sample uses the Microsoft Agent 365 SDK for .NET.

For comprehensive documentation and guidance on building agents with the Microsoft Agent 365 SDK, including how to add tooling, observability, and notifications, visit the Microsoft Agent 365 Developer Documentation.

Prerequisites

  • .NET 8.0 or higher
  • Microsoft Agent 365 SDK
  • Azure/OpenAI API credentials
  • OpenWeather Credentials (if using the OpenWeather Tool)

Working with User Identity

On every incoming message, the A365 platform populates Activity.From with basic user information — always available with no API calls or token acquisition:

Field Description
Activity.From.Id Channel-specific user ID (e.g., 29:1AbcXyz... in Teams)
Activity.From.Name Display name as known to the channel
Activity.From.AadObjectId Azure AD Object ID — use this to call Microsoft Graph

The sample logs these fields at the start of every turn in OnMessageAsync (MyAgent.cs) and injects Activity.From.Name into the LLM system instructions for personalized responses:

var fromAccount = turnContext.Activity.From;
_logger?.LogInformation(
    "Turn received from user — DisplayName: '{Name}', UserId: '{Id}', AadObjectId: '{AadObjectId}'",
    fromAccount?.Name ?? "(unknown)",
    fromAccount?.Id ?? "(unknown)",
    fromAccount?.AadObjectId ?? "(none)");

Handling Agent Install and Uninstall

When a user installs (hires) or uninstalls (removes) the agent, the A365 platform sends an InstallationUpdate activity — also referred to as the agentInstanceCreated event. The sample handles this in OnInstallationUpdateAsync (MyAgent.cs):

Action Description
add Agent was installed — send a welcome message
remove Agent was uninstalled — send a farewell message
if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add)
{
    await turnContext.SendActivityAsync(MessageFactory.Text(AgentHireMessage), cancellationToken);
}
else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove)
{
    await turnContext.SendActivityAsync(MessageFactory.Text(AgentFarewellMessage), cancellationToken);
}

The handler is registered twice in the constructor — once for agentic (A365 production) requests and once for non-agentic (Agents Playground / WebChat) requests, enabling local testing without a full A365 deployment.

To test with Agents Playground, use Mock an Activity → Install application to send a simulated installationUpdate activity.

Sending Multiple Messages in Teams

Agent365 agents can send multiple discrete messages in response to a single user prompt in Teams. This is achieved by calling SendActivityAsync multiple times within a single turn.

Important: Streaming responses are not supported for agentic identities in Teams. The SDK detects agentic identity and buffers the stream into a single message. Use SendActivityAsync directly to send immediate, discrete messages to the user.

The sample demonstrates this in OnMessageAsync (MyAgent.cs) by sending an immediate acknowledgment before the LLM response:

// Message 1: immediate ack — reaches the user right away
await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken);

// ... LLM processing ...

// Message 2: the LLM response (via StreamingResponse, buffered into one message for Teams agentic)
await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);

Each SendActivityAsync call produces a separate Teams message. You can call it as many times as needed to send progress updates, partial results, or a final answer.

Typing Indicators

For long-running operations, send a typing indicator to show a "..." progress animation in Teams:

// Typing indicator loop — refreshes every ~4s for long-running operations.
using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var typingTask = Task.Run(async () =>
{
    try
    {
        while (!typingCts.IsCancellationRequested)
        {
            await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token);
            await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token);
        }
    }
    catch (OperationCanceledException) { /* expected on cancel */ }
}, typingCts.Token);

try { /* ... do work ... */ }
finally
{
    typingCts.Cancel();
    try { await typingTask; } catch (OperationCanceledException) { }
}

Note: Typing indicators are only visible in 1:1 chats and small group chats — not in channels.

Observability

Required OBS-only application credentials

A365 export is disabled by default. To send traces to Agent 365, set EnableAgent365Exporter=true and configure the separate Agent365Observability credentials:

{
  "EnableAgent365Exporter": true,
  "Agent365Observability": {
    "TenantId": "<<AGENT_HOME_TENANT_ID>>",
    "AgentId": "<<AGENT_INSTANCE_CLIENT_ID>>",
    "BlueprintClientId": "<<BLUEPRINT_CLIENT_ID>>",
    "UseManagedIdentity": true,
    "ManagedIdentityClientId": ""
  }
}

AgentId is the actual agent instance application/client ID, not its service principal object ID or the blueprint ID. Empty ManagedIdentityClientId selects the system-assigned identity; set it to a user-assigned managed identity client ID otherwise. This identity must already be configured as a federated credential on the blueprint. For local development, set UseManagedIdentity to false and supply Agent365Observability:BlueprintClientSecret through user secrets, or Agent365Observability__BlueprintClientSecret through the environment. All settings support the standard .NET double-underscore environment syntax. Never commit secrets.

The sample-local OBS provider uses the documented app-only flow: blueprint client_credentials with fmi_path=AgentId and api://AzureADTokenExchange/.default produces T1; agent client_credentials uses T1 as client_assertion for api://9b975845-388f-4429-889e-eab1ef63949c/.default. The existing autonomous sample uses the same protocol through MSAL. No user_fic, OBO, or developer bearer token is used for OBS. Business MCP/Graph authentication and original user/agent baggage are unchanged.

This sample provider is single-instance: one configured tenant and agent instance. Multi-instance or multi-tenant deployments should cache per agent/tenant and reuse the hosting connection credential. The provider requests tokens from login.microsoftonline.com; sovereign clouds need provider changes.

An app-only OBS token with idtyp=app, or without idtyp but with oid equal to sub, may omit roles or have roles: []. Roleless service acceptance requires an **the exact registered Agent 365 agent instance and authorization; selecting S2S or creating an Entra identity alone is insufficient. For registered blueprint agents, do not add an Agent365.Observability.OtelWrite grant solely to populate a roles claim. For AI Teammates, complete the OtelWrite application-role step printed by a365 setup all --aiteammate; AI Teammate S2S without that step has not been validated. Business OBO/MCP/Graph permissions and consent remain independent.

Troubleshooting: Missing/placeholder settings or using the blueprint as AgentId fail when export is enabled; with export disabled, placeholders do not block local/Playground startup. Tenant/agent export mismatches, any delegated scp claim (even empty), explicit non-app/null idtyp, malformed roles, malformed responses, or expired tokens fail closed without a fallback credential. Tokens without idtyp need either oid equal to sub or a valid nonempty array of nonblank string roles. Configure the actual identity represented in the original turn baggage; do not rewrite baggage to bypass a mismatch. For service authorization failures, verify instance registration and authorization; the sample neither provisions identities nor changes permissions. Acquisition has a 30-second bound and expiry-aware caching with a 60-second refresh margin; a failed refresh never returns a stale token.

Build/deployment: The OBS helper files live in this sample's Observability/ folder and are compiled with the project, so copying or publishing the sample is self-contained. Retain the Azure.Identity package alias in the project file.

This sample uses the Microsoft.OpenTelemetry distro. Program.cs creates the provider and enables Agent 365 export only when configured:

using var observabilityTokens = ObservabilityAppTokenFactory.CreateIfEnabled(builder.Configuration);
var agent365ExporterEnabled = observabilityTokens is not null;

builder.UseMicrosoftOpenTelemetry(o =>
{
    o.Exporters = agent365ExporterEnabled ? ExportTarget.Agent365 : (ExportTarget)0;
    if (builder.Environment.IsDevelopment())
    {
        o.Exporters |= ExportTarget.Console;
    }

    if (observabilityTokens is not null)
    {
        o.Agent365.Exporter.UseS2SEndpoint = true;
        o.Agent365.Exporter.TokenResolver = observabilityTokens.ResolveAsync;
    }

    o.Instrumentation.EnableAspNetCoreInstrumentation = true;
    o.Instrumentation.EnableHttpClientInstrumentation = true;
    o.Instrumentation.EnableAzureSdkInstrumentation = true;
});

This produces the following spans automatically — no custom tracing code required:

Span Source What it captures
POST /api/messages ASP.NET Core Inbound request — method, path, status code
POST login.microsoftonline.com HttpClient MSAL token acquisition
POST smba.trafficmanager.net HttpClient Outbound Teams messages
POST …openai.azure.com/…/chat/completions HttpClient Raw Azure OpenAI HTTP call
chat <model> Microsoft.Extensions.AI Full gen_ai.* semantics — model, tool definitions, system prompt, input/output messages, token counts, finish reason
invoke_agent <id> Microsoft.Agents.AI Agent-level span — agent ID, input/output, token counts

The gen_ai.* attributes on the chat span come from .UseOpenTelemetry() on the ChatClientAgent builder in Program.cs. This is the only non-distro observability call in the sample and is worth keeping — it enriches every LLM call with structured semantic data at no cost.

Service Name

By default the OTel SDK sets service.name to unknown_service:<process-name>. Set the standard OTEL_SERVICE_NAME environment variable to give your service a meaningful name in traces:

OTEL_SERVICE_NAME="Agent Framework Sample"

Set this in your local launch profile, deployment environment, or container configuration to match your service catalog.

Running the Agent

To set up and test this agent, refer to the Configure Agent Testing guide for complete instructions.

For a detailed explanation of the agent code and implementation, see the Agent Code Walkthrough.

Support

For issues, questions, or feedback:

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Additional Resources

Trademarks

Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653.

License

Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under the MIT License - see the LICENSE file for details.