Home / Docs / Agents

Agents

Agents

Agents are the core building block of FabrCore. Each agent is a distributed actor backed by an Orleans grain, with its own LLM context, tools, and persistent state. You build agents by extending FabrCoreAgentProxy.

FabrCoreAgentProxy

FabrCoreAgentProxy is a wrapper around the Microsoft Agent Framework's AIAgent that enables building distributed, actor-based AI agents.

Protected Members

MemberTypeDescription
configAgentConfigurationConfiguration passed during creation
fabrAgentHostIFabrCoreAgentHostHost interface for messaging, timers, persistence
serviceProviderIServiceProviderDI service provider
loggerILoggerPre-configured logger instance
configurationIConfigurationApplication configuration

Basic Agent

MyAgent.cs
using FabrCore.Core;
using FabrCore.Sdk;
using Microsoft.Extensions.AI;

[AgentAlias("MyAgent")]
public class MyAgent : FabrCoreAgentProxy
{
    private AIAgent? agent;
    private AgentThread? thread;

    public MyAgent(
        AgentConfiguration config,
        IServiceProvider serviceProvider,
        IFabrCoreAgentHost fabrAgentHost)
        : base(config, serviceProvider, fabrAgentHost) { }

    public override async Task OnInitialize()
    {
        var chatClient = await GetChatClient("default");

        agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
        {
            ChatOptions = new ChatOptions
            {
                Instructions = config.SystemPrompt
            },
            Name = fabrAgentHost.GetHandle()
        })
        .AsBuilder()
        .UseOpenTelemetry(null, cfg => cfg.EnableSensitiveData = true)
        .Build(serviceProvider);

        thread = agent.GetNewThread();
    }

    public override async Task<AgentMessage> OnMessage(AgentMessage message)
    {
        var response = message.Response();
        var chatMessage = new ChatMessage(ChatRole.User, message.Message);
        var result = await agent!.RunAsync(chatMessage, thread);
        response.Message = string.Join("\r\n", result.Messages);
        return response;
    }
}

Helper Methods

GetChatClient

Returns a raw IChatClient for direct LLM access. Use with custom agent frameworks or direct completions.

CreateChatClientAgent

Returns a fully configured ChatClientAgent with automatic message persistence via FabrCoreChatHistoryProvider.

CreateChatClientAgent — With Persistence
(agent, thread) = await CreateChatClientAgent(
    "default",
    threadId: config.Handle,
    tools: [AIFunctionFactory.Create(plugin.MyTool)]
);

Lifecycle

MethodWhen CalledPurpose
OnInitialize()Agent creation / grain activationSet up LLM clients, tools, and state
OnMessage(AgentMessage)Request/response message receivedProcess user messages and return responses
OnEvent(AgentMessage)Fire-and-forget event receivedHandle event notifications from other agents
GetHealth()Health check requestedReturn custom health metrics

AgentThread Patterns

PatternDescriptionUse Case
One thread per agentCreate in OnInitialize, reuseSingle continuous conversation
Thread per userStore in dictionary by user IDMulti-user agents
New thread per messageCall GetNewThread() each timeStateless, independent messages

AgentAlias

Use the [AgentAlias] attribute to register your agent with one or more aliases for routing:

Multiple Aliases
[AgentAlias("CustomerSupport")]
[AgentAlias("support")]
public class CustomerSupportAgent : FabrCoreAgentProxy
{
    // ...
}
Alias Resolution

Multiple aliases can be applied to the same class. Whitespace is trimmed. The alias is used when creating agents via the REST API or ClientContext.

AIContextProvider

Microsoft's AIContextProvider enables dynamic context injection during agent execution. Wire providers directly to each ChatClientAgent for per-agent context control.

Wiring AIContextProvider
var userInfoMemory = new UserInfoMemory(chatClient);

(agent, thread) = await CreateChatClientAgent(
    "default",
    threadId: config.Handle,
    tools: [AIFunctionFactory.Create(plugin.Echo)],
    configureOptions: options =>
    {
        options.AIContextProviderFactory = _ => userInfoMemory;
    }
);

AIContext Properties

PropertyTypeDescription
Instructionsstring?Additional instructions injected into the system prompt
MessagesIList<ChatMessage>?Messages added to the conversation context
ToolsIList<AITool>?Dynamic tools available for this invocation
Common Patterns

Use AIContextProvider for RAG (retrieval augmented generation), dynamic tool injection based on user permissions, and user memory extraction. See the Persistence guide for combining with custom state.

Health Monitoring

Override GetHealth() for custom health metrics:

Custom Health Check
public override ProxyHealthStatus GetHealth()
{
    var state = _errorCount > 10
        ? HealthState.Degraded
        : HealthState.Healthy;

    return new ProxyHealthStatus
    {
        State = state,
        IsInitialized = true,
        ProxyTypeName = GetType().Name,
        CustomMetrics = new Dictionary<string, string>
        {
            { "error_count", _errorCount.ToString() }
        }
    };
}

Health Detail Levels

LevelFields Returned
BasicHandle, State, Timestamp, IsConfigured
Detailed+ AgentType, Uptime, MessagesProcessed, TimerCount, ReminderCount
Full+ ProxyHealth, ActiveStreams, Diagnostics

Agent Discovery

FabrCore provides two scopes for querying agents, each with a different purpose:

APIScopeUse Case
IClientGrain.GetTrackedAgents()Per-clientAgents created by a specific client connection
GET /fabrcoreapi/diagnostics/agentsGlobalAll agents in the cluster (administrative view)
GET /fabrcoreapi/diagnostics/agents/statisticsGlobalAggregate counts by type and status
Intentional Separation

Agents created via the HTTP API are not tracked by any client grain — HTTP requests don't have client context. This is by design. Use GetTrackedAgents() for user-scoped queries and the diagnostics API for administrative views.

Delegation Patterns

Agents that orchestrate other agents can use the health checking and messaging primitives to discover and delegate work:

Health-Based Agent Discovery
private async Task<List<AgentHealthStatus>> GetHealthyAgentsAsync(
    IEnumerable<string> handles)
{
    var results = new List<AgentHealthStatus>();
    foreach (var handle in handles)
    {
        var health = await fabrAgentHost.GetAgentHealth(
            handle, HealthDetailLevel.Detailed);
        if (health.State == HealthState.Healthy && health.IsConfigured)
            results.Add(health);
    }
    return results;
}

// Use SendAndReceiveMessage for synchronous delegation
var result = await fabrAgentHost.SendAndReceiveMessage(new AgentMessage
{
    ToHandle = targetHandle,
    FromHandle = fabrAgentHost.GetHandle(),
    Kind = MessageKind.Request,
    Message = userMessage
});

Actor Model Constraints

FabrCore is built on Orleans, which follows the actor model. This provides strong guarantees but imposes design constraints:

  • Grain isolation: Each agent's state is isolated. No shared mutable state between agents.
  • Single-threaded execution: Grains process one message at a time (with [AlwaysInterleave] exceptions).
  • Location transparency: Grains may run on different silos in a cluster. All communication is via messaging.
Do Not Share Tools Across Agents

Calling tools on one agent from another agent bypasses the message queue, violates single-threaded guarantees, and introduces concurrency hazards. Use SendAndReceiveMessage() for inter-agent communication — it's typically sub-millisecond within the same silo. If two agents need the same tool, configure the same plugin on both.

Documentation