Communication
FabrCore 2.0 · Release and package availability
These guides track the current 2.0 source. Stable 2.0.0 publication is pending; package commands show the release target. Until it is published, follow the source quick start or use a matching available prerelease set. Release migration · Runtime modes
Communication
FabrCore agents communicate through Orleans streams. Messages flow between agents using request/response patterns (AgentChat) and fire-and-forget events (AgentEvent).
Delegate through FabrCore messaging
The SDK's former A2AAgentProxy adapter is removed. Use IFabrCoreAgentHost.SendAndReceiveMessage for an existing FabrCore agent, or harness background delegation for tracked concurrent work. Handles are principal:agent, not agent-type aliases. Provision the target first.
using FabrCore.Core;
using FabrCore.Sdk;
[AgentAlias("orchestrator")]
public sealed class OrchestratorAgent(
AgentConfiguration config, IServiceProvider services, IFabrCoreAgentHost host)
: FabrCoreAgentProxy(config, services, host)
{
public override Task OnInitialize() => Task.CompletedTask;
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var result = await fabrcoreAgentHost.SendAndReceiveMessage(new AgentMessage
{
ToHandle = "support",
Message = message.Message,
Kind = MessageKind.Request
});
var reply = message.Response();
reply.Message = result.Message;
return reply;
}
}For external protocol clients use built-in A2A hosting. For outbound Microsoft agents use the remote-agent integrations. A local Microsoft Agent Framework workflow is a separate application composition, not an Orleans transport adapter.
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? | Operation id used for distributed tracing and verifiable execution |
SpanId | string? | Current message span for causal lineage |
ParentSpanId | string? | Parent message, event, or handler span |
VerifiableExecution | VerifiableExecutionEnvelope? | Compact signed evidence envelope propagated across clusters when enabled |
TraceId, SpanId, and ParentSpanId connect messages and events into a signed operation graph. See Verifiable Execution.
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
Ordinary chat messages arrive at OnMessage; events use OnEvent, and reserved system messages have separate handling. 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 fabrcoreAgentHost.SendMessage(new AgentMessage
{
ToHandle = _callerHandle,
FromHandle = fabrcoreAgentHost.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(EventMessage). Receivers subscribe with AgentConfiguration.Streams entries containing Namespace and Channel; events are routed to a stream, not an agent handle:
await fabrcoreAgentHost.SendEvent(new EventMessage
{
Source = fabrcoreAgentHost.GetHandle(),
Namespace = "AgentEvent",
Channel = "operations",
Type = "status-update",
Data = "Processing",
DataContentType = "text/plain"
});
public override Task OnEvent(EventMessage message)
{
if (message.Type == "status-update")
StatusMessage = message.Data;
return Task.CompletedTask;
}
Timers & Reminders
FabrCore supports two types of scheduled callbacks:
| Feature | Timer | Reminder |
|---|---|---|
| Persistence | No (lost on deactivation) | With a durable Orleans provider; standalone defaults are in memory |
| Minimum period | None | 1 minute |
| Use case | Frequent, short-lived | Infrequent, long-running |
public override async Task OnInitialize()
{
fabrcoreAgentHost.RegisterTimer(
timerName: "heartbeat",
messageType: "timer:heartbeat",
message: null,
dueTime: TimeSpan.FromSeconds(1),
period: TimeSpan.FromSeconds(5));
}
await fabrcoreAgentHost.RegisterReminder(
reminderName: "hourly-report",
messageType: "reminder:hourly-report",
message: null,
dueTime: TimeSpan.FromMinutes(1),
period: TimeSpan.FromHours(1));
Access Control (ACL)
Standalone trusts cross-principal traffic. In SQL mode, FabrCore enables the access-control platform — principals, roles, groups, and permission grants — that governs cross-principal messaging, agent creation, and read access. It has its own documentation page; here is what matters for messaging:
- Same-principal traffic is implicitly allowed; cross-principal traffic is denied by default until a permission grant allows it (an explicit deny always overrides allow).
- Agent-to-agent hops within a principal are trusted; cross-principal agent-to-agent hops are ACL-checked sender-side. The acting principal derives from the sending grain's key — never from the spoofable
FromHandle. Unauthorized sends throwAclDeniedExceptionin Enforce mode. - Transitive fan-out is audited, not blocked: the first cross-principal hop stamps
AgentMessage.CrossPrincipalOrigin/CrossPrincipalHops; chains that cross further principal boundaries emitBoundaryCrossingaudit events and warnings. Kind == Responseand system messages (_status/_error) are exempt from the agent-to-agent check so authorized request/reply round-trips can't be broken.
{ "Subject": { "Kind": "Agent", "Selector": "p1:agent1" }, "Permission": "agent.message.allow", "Resource": "p2:agent3" }
{ "Subject": { "Kind": "Principal", "Selector": "p1" }, "Permission": "agent.message.allow", "Resource": "p2:*" }
{ "Subject": { "Kind": "Principal", "Selector": "p1" }, "Permission": "agent.message.allow", "Resource": "*:agent5" }
The complete model — permission notation, roles, groups, dynamic groups, the System principal, enforcement modes, the management API, application-defined permissions, and security auditing — lives on the Access Control docs page.
Reserved administration channels
In FabrCore 2.0, ordinary chat, event, WebSocket and agent-to-agent ingress reject _admin and _debug. A channel, header or message argument cannot grant admin access. _debug has no defined behavior. Use authenticated diagnostic sessions for read-only operator conversations; their dispatch, state, model usage and replies remain separate from normal message routing.