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
| Member | Type | Description |
|---|---|---|
config | AgentConfiguration | Configuration passed during creation |
fabrAgentHost | IFabrCoreAgentHost | Host interface for messaging, timers, persistence |
serviceProvider | IServiceProvider | DI service provider |
logger | ILogger | Pre-configured logger instance |
configuration | IConfiguration | Application configuration |
Basic Agent
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.
(agent, thread) = await CreateChatClientAgent(
"default",
threadId: config.Handle,
tools: [AIFunctionFactory.Create(plugin.MyTool)]
);
Lifecycle
| Method | When Called | Purpose |
|---|---|---|
OnInitialize() | Agent creation / grain activation | Set up LLM clients, tools, and state |
OnMessage(AgentMessage) | Request/response message received | Process user messages and return responses |
OnEvent(AgentMessage) | Fire-and-forget event received | Handle event notifications from other agents |
GetHealth() | Health check requested | Return custom health metrics |
AgentThread Patterns
| Pattern | Description | Use Case |
|---|---|---|
| One thread per agent | Create in OnInitialize, reuse | Single continuous conversation |
| Thread per user | Store in dictionary by user ID | Multi-user agents |
| New thread per message | Call GetNewThread() each time | Stateless, independent messages |
AgentAlias
Use the [AgentAlias] attribute to register your agent with one or more aliases for routing:
[AgentAlias("CustomerSupport")]
[AgentAlias("support")]
public class CustomerSupportAgent : FabrCoreAgentProxy
{
// ...
}
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.
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
| Property | Type | Description |
|---|---|---|
Instructions | string? | Additional instructions injected into the system prompt |
Messages | IList<ChatMessage>? | Messages added to the conversation context |
Tools | IList<AITool>? | Dynamic tools available for this invocation |
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:
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
| Level | Fields Returned |
|---|---|
Basic | Handle, 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:
| API | Scope | Use Case |
|---|---|---|
IClientGrain.GetTrackedAgents() | Per-client | Agents created by a specific client connection |
GET /fabrcoreapi/diagnostics/agents | Global | All agents in the cluster (administrative view) |
GET /fabrcoreapi/diagnostics/agents/statistics | Global | Aggregate counts by type and status |
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:
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.
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.