Recursive AI

Persist LLM chat history in C#

OneLLM is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Save chat messages to disk, then reload them to continue.

Terminal
dotnet add package RecursiveAI.OneLlm

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using OneLlm;
// runtime and chat: from the full program.
await runtime.Store.SaveAsync(chat.Instance);
var saved = await runtime.Store.LoadAsync(
    chat.InstanceId);

Saves and reloads the messages.

Download this example (.zip): unzip, then dotnet run.

The full program

The download runs offline with canned replies, no API key needed.

gpt-6-luna is a model name in the shipped OpenAI catalog, not a variable defined by this program (model names).

The first runtime saves a chat through FileInstanceStore("chats") and writes its ID to chat-id.txt. A fresh runtime opens that directory and loads the ID. AskRequest.History sends the saved turns before the new question. This is a continuation in a new instance, not a rehydrated ChatSession with the original ID.

C#
using OneLlm;
using OneLlm.Instances;
using OneLlm.Testing;
 
var runtime = new Runtime();
runtime.Store = new FileInstanceStore("chats");
var scripted = new ScriptedProvider("openai");
runtime.Providers.Set("openai", scripted);
scripted.EnqueueText("I’ll remember order 1182.");
using var chat = runtime.Chat(new AskRequest
{
    Model = "gpt-6-luna", System = "You help with orders."
});
Console.WriteLine(await chat.AskAsync("My order is 1182."));
await File.WriteAllTextAsync("chat-id.txt", chat.InstanceId);
 
// A new runtime, as after an application restart.
var resumed = new Runtime();
resumed.Store = new FileInstanceStore("chats");
var next = new ScriptedProvider("openai");
resumed.Providers.Set("openai", next);
next.EnqueueText("Your order is 1182.");
string id = await File.ReadAllTextAsync("chat-id.txt");
var saved = await resumed.Store.LoadAsync(id)
    ?? throw new InvalidOperationException("Chat not found.");
var answer = await resumed.AskAsync(new AskRequest
{
    Model = "gpt-6-luna", System = "You help with orders.",
    History = saved.Messages, Prompt = "What is my order number?"
});
Console.WriteLine(answer.Text);
Console.WriteLine($"Restored messages: {saved.Messages.Count}");
Console.WriteLine($"Messages sent: {next.Requests[0].Messages.Count}");
await File.WriteAllTextAsync("chat-id.txt", answer.Instance.InstanceId);

dotnet run prints:

Text
I’ll remember order 1182.
Your order is 1182.
Restored messages: 2
Messages sent: 3

A missing instance returns null; this program throws InvalidOperationException("Chat not found.") rather than silently starting an empty conversation.

With a real model

The pinned getting-started docs name OPENAI_API_KEY for OpenAI. Set that environment variable to your own credential outside source code. Remove the scripted and next provider declarations, their Providers.Set(...) registrations, and their EnqueueText(...) calls. Also remove the final Messages sent print statement, which reads next.Requests. Keep both FileInstanceStore assignments, the prompts and the history-loading code. Each runtime now uses its built-in provider:

C#
using OneLlm;
var runtime = new Runtime();
var resumed = new Runtime();

This is the replacement runtime initialization, not an additional pair of declarations. OneLLM reads the environment key when the first OpenAI request is made. The second runtime sends the stored messages to the real model along with the new question.

This live-model path was not run here: no API key was used. The output shown above comes from the scripted providers, not from OpenAI.

Variations

Keep the restored conversation open for more turns. Instead of the single AskAsync continuation, use the same history in a new session:

C#
using var continued = resumed.Chat(new AskRequest
{
    Model = "gpt-6-luna", History = saved.Messages,
    System = "You help with orders."
});
Console.WriteLine(await continued.AskAsync("What is my order number?"));

The same scripted answer prints Your order is 1182.. Save continued.InstanceId for the next restart.


The short answer

Set Runtime.Store to a FileInstanceStore, retain the conversation’s instance ID, and load it on the next application start. Pass its messages as AskRequest.History along with the new prompt. Also supply the model and system prompt your application intends to use.

The distinction matters: this recipe resumes the conversation’s context in a new instance. It does not promise a ChatSession.Load API or overwrite the old instance. The download updates chat-id.txt to the new answer’s ID so another continuation can follow the latest branch rather than the original pair of messages.

How to save an AI chat conversation?

Use a durable store before the first turn. The default runtime store is in memory, so keeping an ID alone does not make a conversation survive a process exit. OneLLM’s store documentation describes FileInstanceStore as one YAML file per instance in a directory, written by temp file and rename. The core package includes it; this example needs no database package.

ChatSession.InstanceId identifies the conversation. After the successful turn in the download, the file store contains the user’s order number and the assistant’s reply. Disposing the session releases its turn gate; its instance stays in the store. The next runtime loads the same directory using the ID the application kept.

Keep the ID with the user’s conversation record in a real service, rather than a single shared chat-id.txt. That small file is just the console example’s way of handing the ID between two runtimes. Use your application’s ownership checks before loading history for a user. A conversation ID is a lookup key, not evidence that the caller owns the conversation.

The saved messages are richer than a concatenated transcript. They can include structured parts and tool results. Passing message objects preserves that shape instead of converting every turn into a user-authored string. This recipe uses text only so the stored history and the next request are easy to inspect.

How to maintain context in LLM?

Send the earlier messages with the next question. In this run the saved instance has two messages and the resumed request has three: the earlier user message, the earlier assistant answer, and the new user question. The scripted reply is fixed in advance, so it does not demonstrate a model remembering the order; the request count and saved messages show the context path being exercised.

Supply the system prompt again deliberately. History is the earlier turns; it is not a blanket restore of every runtime setting. Keep your chosen model, prompt policy, tool configuration and store configuration in application code. The request API and the pinned source’s AskRequest distinguish History, System and Prompt.

Long conversations also need a context policy. Persisting every message does not make an arbitrarily long history fit a model’s context window. Decide which turns the next request actually needs before you send it. Preserve the original stored conversation if you derive a shorter working history, and avoid dropping a tool call without its matching result. This example makes no automatic summarization or retrieval claim: it sends the saved messages as they stand.

All onellm recipes