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.
Registry Attributes
FabrCore provides metadata attributes that enrich the discovery registry with information about what your agents can do. This metadata is returned by the /fabrcoreapi/discovery endpoint and helps other agents and users decide whether to interact with a given agent.
| Attribute | Multiplicity | Purpose |
|---|---|---|
[FabrCoreCapabilities("...")] | One per class | Describes what the agent can do — its core responsibilities and features |
[FabrCoreHidden] | One per class | Hides the agent from the discovery endpoint (still usable, just not listed) |
[FabrCoreNote("...")] | Multiple allowed | Usage instructions, prerequisites, or guidance on when not to use this agent |
FabrCoreCapabilities
Use [FabrCoreCapabilities] to describe the agent's core functionality. This is surfaced in the discovery response as the capabilities field:
[AgentAlias("job-agent")]
[Description("Manufacturing job management agent")]
[FabrCoreCapabilities("Manages manufacturing jobs — lookup, status tracking, priority changes, and ship date queries.")]
public class JobAgent : FabrCoreAgentProxy
{
// ...
}
FabrCoreNote
Use [FabrCoreNote] to add usage guidance, prerequisites, or warnings. Multiple notes are allowed and appear as an array in the discovery response:
[AgentAlias("job-agent")]
[FabrCoreNote("Requires a job number in context before most tools will work.")]
[FabrCoreNote("Do not use for quoting — use the quotes-agent instead.")]
public class JobAgent : FabrCoreAgentProxy
{
// ...
}
FabrCoreHidden
Use [FabrCoreHidden] to exclude an agent from the discovery endpoint. The agent remains fully functional — it just won't appear in the registry listing:
[AgentAlias("internal-worker")]
[FabrCoreHidden]
public class InternalWorkerAgent : FabrCoreAgentProxy
{
// Hidden from /fabrcoreapi/discovery but still fully operational
}
These attributes are optional but strongly recommended for any agent that will be discoverable by other agents or surfaced in a registry UI. Combine [Description] (from System.ComponentModel) for a short summary with [FabrCoreCapabilities] for detailed functionality.
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.