Reminders Now Understand Perspective: Notify vs Action
When an agent sets a reminder, who is it for? Is the agent reminding itself to do something, or reminding the user about something? Until today, OpenCaddis didn't distinguish between the two. The reminder fired, the agent got a generic message, and it had to guess what to do with it. We just shipped a change that fixes this — reminders now carry explicit perspective, and the agent behaves differently depending on whether it's acting or notifying.
The Problem
The old RegisterReminder API looked like this:
public async Task<string> RegisterReminder(
string reminderName,
string messageType, // generic — agent had to guess intent
string? message,
double dueTimeMinutes,
double periodMinutes) // always recurring
The LLM had to fill in a raw messageType string and decide on a period — even when the user just wanted a one-time reminder. And when the reminder fired, the AssistantAgent treated it like any other message. The agent didn't know whether to take action or relay a notification. This led to inconsistent behavior: sometimes the agent would try to do work when it should have just notified the user, and sometimes it would just report back when it should have been executing.
The Solution: Two Perspectives
We replaced the generic messageType parameter with a clear reminderType that has exactly two values:
notify
The reminder is for the user. When it fires, the agent's job is to relay the message to the user — nothing more.
"Remind me to check the deployment in 30 minutes"
action
The reminder is for the agent. When it fires, the agent should autonomously execute the task using its available tools.
"Every hour, check the API health endpoint and alert me if it's down"
The new API makes the intent explicit:
public async Task<string> RegisterReminder(
string reminderName,
string reminderType, // "notify" or "action"
string message,
double dueTimeMinutes,
bool recurring = false, // one-shot by default
double periodMinutes = 1) // only used when recurring
How It Works Inside the AssistantAgent
When a reminder fires, the AssistantAgent now formats the message differently based on the reminder type. This is the key change — the agent receives a contextual prompt that tells it exactly what perspective to take:
private static string? FormatReminderMessage(AgentMessage message)
{
if (message.MessageType?.Contains(":action") == true)
return
"[REMINDER TRIGGERED] You have a scheduled task to "
+ "execute autonomously. Use your available tools to "
+ "carry out the following instructions:\n"
+ content;
return
"[REMINDER TRIGGERED] You have a scheduled reminder. "
+ "Notify the user with the following message using "
+ "the SendNotification tool:\n\""
+ content + "\"";
}
With an action reminder, the agent is told to use its tools and execute autonomously. With a notify reminder, it's told to relay the message to the user. The LLM doesn't have to guess — the framing is built into the system.
One-Shot by Default
The other major change is that reminders are now one-shot by default. Most reminders ("remind me in 30 minutes", "check on this in an hour") should fire once and go away. The old API always required a period and always recurred.
Now, recurring defaults to false. One-shot reminders auto-unregister after the first tick:
// After processing the reminder response...
if (message.MessageType?.Contains(":oneshot") == true)
{
var reminderName = message.Args?.GetValueOrDefault("reminderName");
if (reminderName is not null)
await fabrAgentHost.UnregisterReminder(reminderName);
}
Orleans requires a minimum period of 1 minute for all reminders, so one-shot reminders are technically registered with a 1-minute period — but the agent auto-unregisters them after the first tick, so they never fire twice.
The reminder type and one-shot flag are encoded in the message type string: reminder:notify, reminder:action, reminder:notify:oneshot, reminder:action:oneshot. This keeps the information in the message itself so the AssistantAgent can route without external lookups.
Fixing Reminder Delivery to the UI
There was a subtle bug with how reminder responses reached the user. Reminders in FabrCore are self-sent — the agent sends a message to itself on a timer. When OnMessage returns a response, that response goes back to the sender, which is... the agent. The user never sees it.
The fix: after processing a reminder, the AssistantAgent now explicitly sends the response to the connected client:
if (ThinkingNotifier.TryGetClientHandle(myHandle, out var clientHandle))
{
await fabrAgentHost.SendMessage(new AgentMessage
{
ToHandle = clientHandle,
FromHandle = myHandle,
Kind = MessageKind.OneWay,
Message = response.Message
});
}
We also added a keep-alive mechanism in the Chat UI. Orleans observer subscriptions can expire during long idle periods, which means reminder messages would have nowhere to go. The chat component now pings agent health every 2 minutes, which triggers RefreshObserverIfNeeded inside the client context and keeps the subscription alive.
What It Looks Like in Practice
| User Says | Reminder Type | Recurring | Agent Behavior |
|---|---|---|---|
| "Remind me to submit the report in 2 hours" | notify |
No (one-shot) | Fires once, sends notification to user, auto-unregisters |
| "Check the /health endpoint every 15 minutes and tell me if it fails" | action |
Yes | Fires every 15 min, agent calls the endpoint using WebBrowser, reports result |
| "In 30 minutes, summarize my unread emails" | action |
No (one-shot) | Fires once, agent uses Microsoft365Email plugin to read and summarize, auto-unregisters |
| "Every morning at 9am, remind me to check the build" | notify |
Yes | Fires daily, sends a notification to the user each time |
Summary of Changes
| Before | After |
|---|---|
Generic messageType string | Explicit reminderType: "notify" or "action" |
| Always recurring | One-shot by default, opt-in to recurring |
| Agent guesses intent from context | Agent receives a framed prompt matching the perspective |
| Reminder response went back to agent (lost) | Response explicitly sent to connected client |
| Observer subscription could expire | Keep-alive pings every 2 minutes |
| One-shot reminders kept firing | Auto-unregistered after first tick |
These are the kind of changes that look small in a diff but make a real difference in how the system behaves. Reminders went from "sometimes useful, sometimes confusing" to predictable and intentional. The agent knows what perspective to take, the user gets responses reliably, and one-shot reminders clean up after themselves.
Builder of OpenCaddis and the FabrCore framework.