Communication
Communication
FabrCore agents communicate through Orleans streams. Messages flow between agents using request/response patterns (AgentChat) and fire-and-forget events (AgentEvent).
A2AAgentProxy
A2AAgentProxy wraps a remote FabrCoreAgentProxy as a Microsoft Agent Framework AIAgent, allowing FabrCore's distributed Orleans agents to participate in Agent Framework workflows — sequential pipelines, parallel fan-out, hand-off patterns, and more.
[AgentAlias("Orchestrator")]
public class OrchestratorAgent : FabrCoreAgentProxy
{
private A2AAgentProxy? supportAgent;
private A2AAgentProxy? reviewAgent;
public override async Task OnInitialize()
{
// Wrap remote FabrCore agents as AIAgent instances
// Format: "agentType:agentHandle"
supportAgent = new A2AAgentProxy(
fabrAgentHost, "CustomerSupport:support1");
reviewAgent = new A2AAgentProxy(
fabrAgentHost, "ReviewAgent:reviewer1");
}
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var response = message.Response();
// Use in a Microsoft Agent Framework workflow
var workflow = new WorkflowBuilder(supportAgent)
.AddEdge(supportAgent, reviewAgent)
.Build();
var result = await InProcessExecution.RunAsync(
workflow, new ChatMessage(ChatRole.User, message.Message));
response.Message = result.Text;
return response;
}
}
FabrCore agents run as Orleans grains on a distributed cluster. A2AAgentProxy bridges the gap by implementing the AIAgent interface, so your FabrCore agents can be used anywhere the Microsoft Agent Framework expects an agent — WorkflowBuilder, sequential pipelines, parallel fan-out, conditional routing, and more.
IFabrCoreAgentHost Messaging Methods
| Method | Description |
|---|---|
SendAndReceiveMessage(AgentMessage) | Send a request and wait for a response |
SendMessage(AgentMessage) | Send a one-way message (no response expected) |
SendEvent(AgentMessage) | Send a fire-and-forget event notification |
AgentMessage Structure
| Property | Type | Description |
|---|---|---|
Id | string | Unique message identifier (auto-generated GUID) |
ToHandle | string? | Target agent handle |
FromHandle | string? | Sender agent handle |
OnBehalfOfHandle | string? | Original sender when proxying |
Channel | string? | Message channel/topic |
MessageType | string? | Custom message type identifier |
Message | string? | Text content |
Kind | MessageKind | Request, OneWay, or Response |
DataType | string? | Type identifier for Data payload |
Data | byte[]? | Binary data payload |
Files | List<string> | File identifiers |
State | Dictionary<string, string>? | Custom key-value state |
TraceId | string? | Distributed tracing identifier |
Creating Response Messages
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var response = message.Response(); // Copies Id, TraceId, sets Kind=Response
response.Message = "Your response here";
return response;
}
Message Routing Patterns
All messages arrive at the OnMessage entry point. Use the message fields to route to the appropriate handler:
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
// Route by channel
if (string.Equals(message.Channel, "agent",
StringComparison.OrdinalIgnoreCase))
return await HandleAgentResponse(message);
// Ignore fire-and-forget notifications
if (message.Kind == MessageKind.OneWay)
return message.Response();
// Default: handle user messages
return await HandleUserMessage(message);
}
| Field | Type | Use For |
|---|---|---|
Channel | string? | Topic-based routing (e.g., "agent", "system") |
MessageType | string? | Custom type identifiers (e.g., "thinking", "status") |
Kind | MessageKind | Request vs OneWay vs Response |
The explicit filtering pattern is intentional. Each agent has different routing rules — these 5-8 lines of if statements ARE your agent's business logic, not framework boilerplate. Resist the urge to over-abstract.
Progress Notifications
Send progress updates to the calling client during long-running operations using SendMessage with OneWay kind:
private string? _callerHandle;
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
_callerHandle = message.FromHandle;
await SendProgressAsync("Analyzing your request...");
// ... do work
await SendProgressAsync("Processing results...");
// ... return response
}
private async Task SendProgressAsync(string text)
{
if (_callerHandle is null) return;
await fabrAgentHost.SendMessage(new AgentMessage
{
ToHandle = _callerHandle,
FromHandle = fabrAgentHost.GetHandle(),
Kind = MessageKind.OneWay,
MessageType = "thinking",
Message = text
});
}
The incoming message's FromHandle provides the caller context. Track it as an instance field — no static dictionaries or helper classes required.
Events
Events are fire-and-forget messages handled by OnEvent:
await fabrAgentHost.SendEvent(new AgentMessage
{
FromHandle = fabrAgentHost.GetHandle(),
ToHandle = "monitor-agent",
MessageType = "status-update",
Message = "{ \"status\": \"processing\" }"
});
public override async Task OnEvent(AgentMessage message)
{
switch (message.MessageType)
{
case "status-update":
await HandleStatusUpdate(message);
break;
case "notification":
await HandleNotification(message);
break;
}
}
Timers & Reminders
FabrCore supports two types of scheduled callbacks:
| Feature | Timer | Reminder |
|---|---|---|
| Persistence | No (lost on deactivation) | Yes (survives restarts) |
| Minimum period | None | 1 minute |
| Use case | Frequent, short-lived | Infrequent, long-running |
public override async Task OnInitialize()
{
fabrAgentHost.RegisterTimer(
timerName: "heartbeat",
messageType: "timer:heartbeat",
message: null,
dueTime: TimeSpan.FromSeconds(1),
period: TimeSpan.FromSeconds(5));
}
await fabrAgentHost.RegisterReminder(
reminderName: "hourly-report",
messageType: "reminder:hourly-report",
message: null,
dueTime: TimeSpan.FromMinutes(1),
period: TimeSpan.FromHours(1));
Access Control (ACL)
FabrCore includes an access control layer that governs which clients can interact with agents owned by other users. By default, agents are scoped to their owner — own-agent access is always allowed with zero overhead. Cross-owner access requires explicit ACL rules.
IAclProvider Interface
ACL evaluation is handled by the IAclProvider interface. The default implementation is InMemoryAclProvider, which loads rules from the Acl section of fabrcore.json at startup. You can replace it with a custom provider (e.g., SQL-backed) for persistent rule management.
builder.AddFabrCoreServer(new FabrCoreServerOptions
{
AdditionalAssemblies = [typeof(MyAgent).Assembly]
}
.UseAclProvider<SqlAclProvider>());
AclRule Structure
Each rule specifies a pattern for the target agent's owner, the agent alias, the caller, and the permissions granted:
| Property | Type | Description |
|---|---|---|
OwnerPattern | string | Pattern matching the target agent's owner (e.g., "system", "*") |
AgentPattern | string | Pattern matching the target agent's alias (e.g., "analytics_*", "*") |
CallerPattern | string | Who is allowed — exact owner ID, "*" for all, or "group:name" for group membership |
Permission | AclPermission | Flags: Message, Configure, Read, Admin, or All |
Evaluation Order
- Own-agent check — if the caller owns the target agent, access is allowed with full permissions (zero-overhead short-circuit)
- Rule scan — rules are evaluated in order; first match wins
- No match — access is denied
If no ACL rules are configured, a default seed rule system:* -> * -> Message,Read is applied, allowing all callers to message and read system-owned agents.
ACL Configuration Example
{
"Acl": {
"Rules": [
{
"OwnerPattern": "system",
"AgentPattern": "*",
"CallerPattern": "*",
"Permission": "Message,Read"
},
{
"OwnerPattern": "shared",
"AgentPattern": "analytics_*",
"CallerPattern": "group:premium",
"Permission": "Message,Read"
}
],
"Groups": {
"admins": ["alice", "bob"],
"premium": ["alice", "charlie"]
}
}
}
Permissions
| Permission | Value | Description |
|---|---|---|
Message | 1 | Send messages to the agent |
Configure | 2 | Create or reconfigure the agent |
Read | 4 | Read threads, state, and health |
Admin | 8 | Modify ACL rules |
All | 15 | All permissions combined |
Enforcement Points
| Method | Permission Required | Notes |
|---|---|---|
SendAndReceiveMessage | Message | Checked in ClientGrain for cross-owner calls |
SendMessage | Message | Checked in ClientGrain for cross-owner calls |
CreateAgent | Configure | Cross-owner agent creation only |
Agent-to-agent communication within the Orleans cluster is trusted and bypasses ACL checks entirely. ACL enforcement applies only to client-to-agent calls that cross owner boundaries.