MODULE 02 · LESSON 2.1
Separate the FabrCore agent from the AI agent
Choose what belongs in the managed agent and what belongs in its AI runtime.
Lesson 6 of 86 · FabrCore 2.0
Overview
FabrCoreAgentProxy is the application boundary: lifecycle, messages, state, tools and host context. AIAgent is the reasoning component inside it. An agent can be deterministic, use one model, or compose specialists without changing its external handle contract.
The proxy owns the application contract
FabrCoreAgentProxy is the object your application implements. The Host gives it configuration and trusted services, calls OnInitialize for setup, and sends AgentMessage objects to OnMessage. AIAgent is a component your proxy may construct to perform inference. Keeping those roles separate lets the same application expose an echo agent, a model-backed assistant, or an orchestrator through the same addressing system.
What happens when a second message arrives
A model call can take time. Another arrival while the agent is busy is a routing/lifecycle case, not automatically another independent conversation. OnMessageBusy lets an implementation respond to that situation under the framework's busy behavior. Avoid mutating the same session as though parallel arrivals were unrelated. The example is a lifecycle customization, not a replacement you must paste into every agent.
Read the agent's lifecycle
- Keep business routing and authorization in the proxy. Create the chat client/session during OnInitialize rather than rebuilding them on every message.
- Add Description, FabrCoreCapabilities and FabrCoreNote metadata to the class. Describe when callers should use it and when they should not.
- Override OnMessageBusy only when the default busy response is insufficient. Avoid mutating state used by the active OnMessage turn.
// Example: Acknowledge receipt and tell the caller what's happening
public override Task<AgentMessage> OnMessageBusy(AgentMessage message)
{
var primaryMsg = ActiveMessage;
var response = new AgentMessage
{
ToHandle = message.FromHandle,
FromHandle = config.Handle,
OnBehalfOfHandle = message.OnBehalfOfHandle,
Message = $"I'm currently processing a request from {primaryMsg?.FromHandle ?? "another user"}. " +
"I'll be available shortly.",
MessageType = message.MessageType,
Kind = MessageKind.Response
};
// Stamp W3C trace fields from the ambient Activity — do NOT hand-copy message.TraceId.
// The grain's OnMessageBusy ingress already opened an Activity; this keeps the response in the same trace.
response.StampFromActivity(Activity.Current);
return Task.FromResult(response);
}
// Example: Route timer messages differently when busy
public override Task<AgentMessage> OnMessageBusy(AgentMessage message)
{
// Timer messages can be identified by their MessageType
if (message.MessageType?.StartsWith("timer:") == true)
{
// Skip timer work when busy — the next tick will catch up
return Task.FromResult(message.Response());
}
// Default busy response for user messages
return base.OnMessageBusy(message);
}
Observe initialization and a turn
- Place a breakpoint or development log in OnInitialize and OnMessage of the echo agent. Create/call its instance and identify which method performs setup and which handles the request.
- Send a second ordinary message without resetting the instance. OnMessage should handle the new input; do not depend on setup running for every turn.
- For a busy-behavior experiment, use a disposable agent with a controlled slow operation and send another request before it finishes. Record whether the second request is rejected, handled or routed by your selected busy policy.
The visible distinction is initialization versus per-message work. Model construction belongs to the former; processing the user's message belongs to the latter. The busy policy determines how overlapping work is represented.
If the result is different
Do not confuse a model's tool loop with the Host's message lifecycle. Health/readiness and disposal still belong to the managed application boundary.
Go deeper
Explore the related documentation.