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?Distributed tracing identifier

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 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.

C# — Custom ACL Provider Registration
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:

PropertyTypeDescription
OwnerPatternstringPattern matching the target agent's owner (e.g., "system", "*")
AgentPatternstringPattern matching the target agent's alias (e.g., "analytics_*", "*")
CallerPatternstringWho is allowed — exact owner ID, "*" for all, or "group:name" for group membership
PermissionAclPermissionFlags: Message, Configure, Read, Admin, or All

Evaluation Order

  1. Own-agent check — if the caller owns the target agent, access is allowed with full permissions (zero-overhead short-circuit)
  2. Rule scan — rules are evaluated in order; first match wins
  3. No match — access is denied
Default Seed Rule

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

C# — fabrcore.json ACL Rules
{
  "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

PermissionValueDescription
Message1Send messages to the agent
Configure2Create or reconfigure the agent
Read4Read threads, state, and health
Admin8Modify ACL rules
All15All permissions combined

Enforcement Points

MethodPermission RequiredNotes
SendAndReceiveMessageMessageChecked in ClientGrain for cross-owner calls
SendMessageMessageChecked in ClientGrain for cross-owner calls
CreateAgentConfigureCross-owner agent creation only
Agent-to-Agent is Trusted

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.

Documentation