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));