Skip to content
Development
Agent

custom-agent

Learn how to build custom agents with Microsoft Agent Framework.

From plugin
dotnet-skills
46650 skills50 agents
Install
$ npx -y skills add managedcode/dotnet-skills --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Learn how to build custom agents with Microsoft Agent Framework.

Agent definition

custom-agent.md
title: Custom Agents
description: Learn how to build custom agents with Microsoft Agent Framework.
zone_pivot_groups: programming-languages
author: westey-m
ms.topic: tutorial
ms.author: westey
ms.date: 09/25/2025
ms.service: agent-framework

Custom Agents

::: zone pivot="programming-language-csharp"

Microsoft Agent Framework supports building custom agents by inheriting from the `AIAgent` class and implementing the required methods.

This article shows how to build a simple custom agent that parrots back user input in upper case. In most cases building your own agent will involve more complex logic and integration with an AI service.

Getting Started

Add the required NuGet packages to your project.

dotnet add package Microsoft.Agents.AI.Abstractions --prerelease

Create a Custom Agent

The Agent Thread

To create a custom agent you also need a thread, which is used to keep track of the state of a single conversation, including message history, and any other state the agent needs to maintain.

To make it easy to get started, you can inherit from various base classes that implement common thread storage mechanisms.

1. `InMemoryAgentThread` - stores the chat history in memory and can be serialized to JSON. 1. `ServiceIdAgentThread` - doesn't store any chat history, but allows you to associate an ID with the thread, under which the chat history can be stored externally.

For this example, you'll use the `InMemoryAgentThread` as the base class for the custom thread.

internal sealed class CustomAgentThread : InMemoryAgentThread
{
    internal CustomAgentThread() : base() { }
    internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
        : base(serializedThreadState, jsonSerializerOptions) { }
}

The Agent class

Next, create the agent class itself by inheriting from the `AIAgent` class.

internal sealed class UpperCaseParrotAgent : AIAgent
{
}

Constructing threads

Threads are always created via two factory methods on the agent class. This allows for the agent to control how threads are created and deserialized. Agents can therefore attach any additional state or behaviors needed to the thread when constructed.

Two methods are required to be implemented:

    public override Task<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) 
        => Task.FromResult<AgentThread>(new CustomAgentThread());

    public override Task<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
        => Task.FromResult<AgentThread>(new CustomAgentThread(serializedThread, jsonSerializerOptions));

Core agent logic

The core logic of the agent is to take any input messages, convert their text to upper case, and return them as response messages.

Add the following method to contain this logic. The input messages are cloned, since various aspects of the input messages have to be modified to be valid response messages. For example, the role has to be changed to `Assistant`.

    private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string agentName) => messages.Select(x =>
        {
            var messageClone = x.Clone();
            messageClone.Role = ChatRole.Assistant;
            messageClone.MessageId = Guid.NewGuid().ToString();
            messageClone.AuthorName = agentName;
            messageClone.Contents = x.Contents.Select(c => c is TextContent tc ? new TextContent(tc.Text.ToUpperInvariant())
            {
                AdditionalProperties = tc.AdditionalProperties,
                Annotations = tc.Annotations,
                RawRepresentation = tc.RawRepresentation
            } : c).ToList();
            return messageClone;
        });

Agent run methods

Finally, you need to implement the two core methods that are used to run the agent: one for non-streaming and one for streaming.

For both methods, you need to ensure that a thread is provided, and if not, create a new thread. The thread can then be updated with the new messages by calling `NotifyThreadOfNewMessagesAsync`. If you don't do this, the user won't be able to have a multi-turn conversation with the agent and each run will be a fresh interaction.

    public override async Task<AgentResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
    {
        thread ??= await this.GetNewThreadAsync(cancellationToken);
        List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
        await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
        return new AgentResponse
        {
            AgentId = this.Id,
            ResponseId = Guid.NewGuid().ToString(),
            Messages = responseMessages
        };
    }

    public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        thread ??= await this.GetNewThreadAsync(cancellationToken);
        List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
        await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
        foreach (var message in responseMessages)
        {
            yield return new AgentResponseUpdate
            {
                AgentId = this.Id,
                AuthorName = this.DisplayName,
                Role = ChatRole.Assistant,
                Contents = message.Contents,
                ResponseId = Guid.NewGuid().ToString(),
Read more
Ships withdotnet-skills

Stop explaining .NET to your AI. Start building. We've all been there: asking Claude to use Entity Framework, only to get EF6 patterns in a .NET 8 project. Explaining to Copilot that Blazor Server and Blazor WebAssembly aren't the same thing.

Get the whole plugin