Skip to content

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

PropertyTypeDescription
IdstringUnique message identifier (auto-generated GUID)
ToHandlestring?Target agent handle
FromHandlestring?Sender agent handle
OnBehalfOfHandlestring?Original sender when proxying
Channelstring?Message channel/topic
MessageTypestring?Custom message type identifier
Messagestring?Text content
KindMessageKindRequest, OneWay, or Response
DataTypestring?Type identifier for Data payload
Databyte[]?Binary data payload
FilesList<string>File identifiers
StateDictionary<string, string>?Custom key-value state
TraceIdstring?Operation id used for distributed tracing and verifiable execution
SpanIdstring?Current message span for causal lineage
ParentSpanIdstring?Parent message, event, or handler span
VerifiableExecutionVerifiableExecutionEnvelope?Compact signed evidence envelope propagated across clusters when enabled
Verifiable lineage: when verifiable execution is enabled, TraceId, SpanId, and ParentSpanId connect messages and events into a signed operation graph. See Verifiable Execution.

Creating Response Messages

Response Helper
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:

Common Routing Pattern
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);
}
FieldTypeUse For
Channelstring?Topic-based routing (e.g., "agent", "system")
MessageTypestring?Custom type identifiers (e.g., "thinking", "status")
KindMessageKindRequest vs OneWay vs Response
Keep It Simple

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:

Sending Progress Updates
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
    });
}
No Static State Needed

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:

Sending Events
await fabrcoreAgentHost.SendEvent(new EventMessage
{
    Source = fabrcoreAgentHost.GetHandle(),
    Namespace = "AgentEvent",
    Channel = "operations",
    Type = "status-update",
    Data = "Processing",
    DataContentType = "text/plain"
});
Handling Events
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:

FeatureTimerReminder
PersistenceNo (lost on deactivation)With a durable Orleans provider; standalone defaults are in memory
Minimum periodNone1 minute
Use caseFrequent, short-livedInfrequent, long-running
Timer Registration
public override async Task OnInitialize()
{
    fabrcoreAgentHost.RegisterTimer(
        timerName: "heartbeat",
        messageType: "timer:heartbeat",
        message: null,
        dueTime: TimeSpan.FromSeconds(1),
        period: TimeSpan.FromSeconds(5));
}
Reminder Registration
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 throw AclDeniedException in Enforce mode.
  • Transitive fan-out is audited, not blocked: the first cross-principal hop stamps AgentMessage.CrossPrincipalOrigin/CrossPrincipalHops; chains that cross further principal boundaries emit BoundaryCrossing audit events and warnings.
  • Kind == Response and system messages (_status/_error) are exempt from the agent-to-agent check so authorized request/reply round-trips can't be broken.
JSON — Cross-talk grant shapes
{ "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" }
Full ACL documentation

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.