Persistence
FabrCore 2.0 · Release and package availability
These guides track the current 2.0 source. Stable 2.0.0 publication is pending; package commands show the release target. Until it is published, follow the source quick start or use a matching available prerelease set. Release migration · Runtime modes
Default standalone Orleans state is in memory and lost on restart. SQL mode supplies durable Orleans defaults; explicit Azure/custom storage remains supported. ACL, Memory, GraphRAG and operational stores have their own SQL schemas.
Persistence
FabrCore provides built-in persistence through Orleans grain state, enabling conversation history and custom data across activations. Restart durability requires a persistent provider; default standalone storage is in memory.
FabrCoreChatHistoryProvider
FabrCoreChatHistoryProvider implements the framework history-provider integration with Orleans-backed persistence. It's automatically wired when using CreateChatClientAgent.
Messages are held in memory until flush — fast, no I/O per message.
History is loaded from Orleans state on first access, not on initialization.
Automatic Persistence (Recommended)
using FabrCore.Core;
using FabrCore.Sdk;
using Microsoft.Agents.AI;
[AgentAlias("persistent-agent")]
public sealed class PersistentAgent(
AgentConfiguration config, IServiceProvider services, IFabrCoreAgentHost host)
: FabrCoreAgentProxy(config, services, host)
{
private ChatClientAgentResult chat = null!;
public override async Task OnInitialize()
=> chat = await CreateChatClientAgent("default", "main");
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var result = await chat.Agent.RunAsync(message.Message ?? "", chat.Session);
var response = message.Response();
response.Message = result.Text;
return response;
}
}
After each OnMessage completes, the Orleans grain automatically calls FlushAsync() on all tracked stores. On grain deactivation, any remaining pending messages are flushed during graceful deactivation; abrupt termination can still lose unflushed data.
Thread ID Strategies
| Strategy | Thread ID | Use Case |
|---|---|---|
| One per agent | config.Handle | Personal assistant, single-user agents |
| Per-user | message.FromHandle | Multi-user agents |
| Per-channel | message.Channel ?? "default" | Channel-based chat |
| Combined | $"{fromHandle}:{channel}" | User-specific within channels |
API Reference
| Method | Description |
|---|---|
AddMessagesAsync(messages) | Add messages to in-memory buffer (fast, no I/O) |
GetMessagesAsync() | Get all messages (persisted + pending), lazy-loads from Orleans |
FlushAsync() | Persist pending messages to Orleans grain state |
HasPendingMessages | Returns true if there are unsaved messages |
ThreadId | The unique thread identifier |
Custom State
Persist arbitrary typed data (user info, counters, preferences) that survives grain deactivation:
| Method | Description |
|---|---|
GetStateAsync<T>(key) | Get a strongly-typed value by key |
GetStateOrCreateAsync<T>(key, factory) | Get existing value or create with factory |
HasStateAsync(key) | Check if a key exists |
SetState<T>(key, value) | Set a value (buffered until flush) |
RemoveState(key) | Remove a key (buffered until flush) |
FlushStateAsync() | Persist all pending changes to Orleans |
public override async Task<AgentMessage> OnMessage(AgentMessage message)
{
var response = message.Response();
// Get or create pattern
var stats = await GetStateOrCreateAsync(
"stats", () => new ConversationStats());
stats.MessageCount++;
stats.LastMessage = DateTime.UtcNow;
SetState("stats", stats);
// Process message...
var result = await agent!.RunAsync(message.Message, thread);
response.Message = result.Text;
await FlushStateAsync();
return response;
}
Group related state updates and call FlushStateAsync() once at the end of OnMessage. On grain deactivation, pending changes are auto-flushed during graceful shutdown; failures must still be handled.
Validated history compaction
2.0 separates per-model-call working context from durable conversation history. Older tool results become bounded excerpts at 50% and 80% of the input working set; full originals remain in stored history until durable compaction.
Automatic durable compaction uses 70% of a usable working set, with a 75% fallback. It preserves instructions, the latest user message and latest interaction group in a single validated write. Invalid or stale summary results leave original history intact. See context and history compaction for metadata, overrides and safeguards.
Orleans Storage Providers
Message threads and custom state are stored in Orleans grain state. Supported providers:
- In-memory — Development only, data lost on restart (built into
FabrCore.Host) - SQL Server — Production, durable, with automatic table deployment (
FabrCore.Hostpackage) - Azure Storage — Cloud-native; grain state in Blob Storage by default (subject to Azure Blob and serializer limits), Table Storage opt-in (
FabrCore.Host.AzureStoragepackage) - Custom Orleans providers — PostgreSQL, MySQL, Cosmos DB, etc. via the advanced Orleans path or a custom
IFabrCoreOrleansProvider
SQL Server is integrated in Host; Azure uses its separate provider. The feature database enables SQL defaults unless explicitly overridden. Configure schema provisioning deliberately. See Orleans Provider Packages in the server docs.
Protect stored connection grants
Host selects credential protection from effective persistence. In-memory localhost loses keys and grants on restart. SQL shares encrypted key XML; supply a private-key certificate to every silo and retain previous certificates during rotation. Read the integration guide.