Home / Docs / Communication

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.

Using A2AAgentProxy in a Workflow
[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;
    }
}
Why A2AAgentProxy?

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

MethodDescription
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

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

All messages arrive at the OnMessage entry point. 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 fabrAgentHost.SendMessage(new AgentMessage
    {
        ToHandle = _callerHandle,
        FromHandle = fabrAgentHost.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:

Sending Events
await fabrAgentHost.SendEvent(new AgentMessage
{
    FromHandle = fabrAgentHost.GetHandle(),
    ToHandle = "monitor-agent",
    MessageType = "status-update",
    Message = "{ \"status\": \"processing\" }"
});
Handling Events
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:

FeatureTimerReminder
PersistenceNo (lost on deactivation)Yes (survives restarts)
Minimum periodNone1 minute
Use caseFrequent, short-livedInfrequent, long-running
Timer Registration
public override async Task OnInitialize()
{
    fabrAgentHost.RegisterTimer(
        timerName: "heartbeat",
        messageType: "timer:heartbeat",
        message: null,
        dueTime: TimeSpan.FromSeconds(1),
        period: TimeSpan.FromSeconds(5));
}
Reminder Registration
await fabrAgentHost.RegisterReminder(
    reminderName: "hourly-report",
    messageType: "reminder:hourly-report",
    message: null,
    dueTime: TimeSpan.FromMinutes(1),
    period: TimeSpan.FromHours(1));

Access Control (ACL)

FabrCore ships a full 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": "agent:p1:agent1", "Permission": "agent.message.allow", "Resource": "p2:agent3" }
{ "Subject": "principal:p1",    "Permission": "agent.message.allow", "Resource": "p2:*" }
{ "Subject": "principal: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.

Documentation