Database

Introduction

Overview

Persistence lets an agent outlive a single session. The conversation history, model name, and compaction status are serialized as JSON that can be stored in a file, a database column, or transmitted over a network. Four storage options are available:

  • Automatic filesystem persistence: set path: on an agent for transparent auto-save after every turn (recommended for most file-based use-cases)
  • Filesystem: save and restore from a JSON file on disk
  • ActiveRecord: persist state in a database column using acts_as_agent
  • Sequel: persist state in a database column using plugin :agent

All three use the same serialization mechanism under the hood.

How it works

LLM::Context implements LLM::Context#to_h and LLM::Context#to_json for serialization and LLM::Context#restore for deserialization. Save writes the current state, restore loads it back and picks up where the conversation left off. The ORM wrappers automate this; each LLM::Agent#talk or LLM::Agent#ask call persists the updated state back to the column automatically.

Why would I use it?

Without persistence, every agent starts with a blank conversation. Persistence enables long-running agents that survive process restarts, debugging sessions that resume mid-investigation, and conversation history that can be queried alongside application data.

Notes

The saved JSON can be stored in a file, a database column, or transmitted over the network. The ORM integrations use the same underlying serialization as filesystem persistence.

Automatic filesystem persistence

Overview

The LLM::Agent#path attribute provides transparent auto-persistence. Set a file path once and the agent restores conversation history from that file on startup and saves it back after every LLM::Agent#talk or LLM::Agent#ask turn. No manual LLM::Agent#save/ LLM::Agent#restore calls needed.

The feature is available on subclasses via the class DSL and on direct instances via the path: keyword argument.

How it works

When a path is set, the agent loads existing state from the file during initialization. After each turn (LLM::Agent#talk or LLM::Agent#ask), the updated state is written back automatically. If the file does not exist yet, the agent starts with a blank conversation and creates the file on the first save.

The path can be set using any of these approaches:

Class DSL
class PersistentAgent < LLM::Agent
  set model: "deepseek-v4-pro",
      path: "session.json"
end

llm = LLM.deepseek(key: ENV["KEY"])
agent = PersistentAgent.new(llm)
agent.talk "remember my name is robert"
# state saved automatically to session.json

agent2 = PersistentAgent.new(llm)
agent2.talk "what's my name?"
# restored from session.json; prints "robert"
Keyword argument
llm = LLM.deepseek(key: ENV["KEY"])
agent = LLM::Agent.new(llm, path: "session.json", stream: $stdout)
agent.talk "remember my name is robert"
# state saved automatically
Lazy resolution
class DynamicAgent < LLM::Agent
  set path: -> { "sessions/#{name}.json" }
end

Why would I use it?

Auto-persistence eliminates boilerplate. You set the path once and forget about serialization entirely. The agent picks up where it left off across process restarts, REPL sessions, or debugging runs without a single LLM::Agent#save or LLM::Agent#restore call in your code.

Notes

The auto-path feature is a strict superset of manual filesystem persistence. If you need fine-grained control over when state is saved (e.g. batch several turns before persisting), use the manual LLM::Agent#save and LLM::Agent#restore methods described in the Filesystem section below. The path attribute delegates to the same underlying serialization.

Filesystem

Overview

A conversation that ends when the process exits is not very useful. Serialization saves the context (message history, model name, compaction status) as JSON that can be stored in a string, a file, or a database column. Restore it later, in a different process or on a different machine, and pick up where you left off.

How it works

LLM::Context implements LLM::Context#to_h and LLM::Context#to_json for serialization and LLM::Context#restore for deserialization. The serialized state includes the message history, model name, and compaction status. Save and restore work with file paths or in-memory strings. You can also serialize to a JSON string for database storage or network transmission:

Save to a file
llm = LLM.deepseek(key: ENV["KEY"])
agent = LLM::Agent.new(llm)
agent.talk "remember my name is robert"
agent.save(path: "agent.json")
Restore from a file
llm = LLM.deepseek(key: ENV["KEY"])
agent = LLM::Agent.new(llm, stream: $stdout)
agent.restore(path: "agent.json")
agent.talk "what's my name?"
Serialize to a JSON string
llm = LLM.deepseek(key: ENV["KEY"])
agent = LLM::Agent.new(llm)
agent.talk "remember my name is robert"
json = agent.save
agent2 = LLM::Agent.new(llm)
agent2.restore(string: json)

Why would I use it?

Filesystem persistence is the foundation for long-running agents that outlive a single session. Save a debugging session mid-investigation and resume it tomorrow. Store a completed conversation as evidence or training data. Pass context between processes, between machines, or through a job queue.

Notes

LLM::Agent delegates serialization to its internal context. The saved JSON can be stored in a file, a database column, or transmitted over the network.

ActiveRecord

Overview

ActiveRecord models use LLM::ActiveRecord#acts_as_agent to install the agent wrapper. The method accepts a block for configuring agent defaults and an options hash for storage format. Most defaults such as model, tools, instructions, schema, and concurrency can be set through the block. Tracer and stream can also be set here rather than through legacy convention methods.

How it works

When you want to add agent persistence to an ActiveRecord model, call LLM::ActiveRecord#acts_as_agent in the model class. The data column stores the full agent state (conversation history, model name, compaction status) as JSON. On first call, a fresh agent is created and the conversation starts from scratch.

On subsequent calls, the stored state is restored and the conversation continues. Every LLM::Agent#talk or LLM::Agent#ask call persists the updated state back to the column automatically.

The data_column: option lets you use a different column name. The format: option controls the storage type. Use :string for a text column (any database) or :jsonb for a native PostgreSQL JSONB column (recommended for PostgreSQL). For :jsonb, ActiveRecord handles JSON typecasting automatically.

The block yields an LLM::Agent instance. Set agent defaults in the block using the LLM::Agent.set method or individual accessors. Anything you can set on an agent can be configured here – model, tools, instructions, schema, concurrency, tracer, stream, and confirm:

Note that tracer and stream are set directly in the block rather than through legacy set_tracer / set_context convention methods. The block style is the recommended approach for all agent configuration. Only set_provider is required as a private method.

Migration with TEXT column
class CreateAgents < ActiveRecord::Migration[7.1]
  def change
    create_table :agents do |t|
      t.string :name
      t.text :data   # stores the serialized agent state
      t.timestamps
    end
  end
end
Migration with JSONB column
class CreateAgents < ActiveRecord::Migration[7.1]
  def change
    create_table :agents do |t|
      t.string :name
      t.jsonb :data  # native JSON storage, supports indexing
      t.timestamps
    end
  end
end
Model setup
require "active_record"
require "llm"
require "llm/active_record"

class Agent < ApplicationRecord
  acts_as_agent(format: :jsonb) do |agent|
    agent.model "deepseek-v4-pro"
    agent.instructions "solve the user's query"
    agent.tools [Research, FinalizeResearch, ActOnResearch]
    agent.tracer LLM::Tracer::Logger.new(llm, io: $stdout)
  end

  private

  def set_provider
    LLM.deepseek(key: ENV["KEY"])
  end
end

agent = Agent.create!(name: "researcher")
agent.talk "perform research"
# state is saved to the data column automatically

Why would I use it?

The ActiveRecord wrapper integrates with your existing models. Agent state is automatically persisted after each conversation turn using the same create!, save!, and query methods you already use.

Notes

The format option defaults to :string. Use :json or :jsonb for PostgreSQL. JSONB is recommended; it supports indexing and is more efficient for querying. The data_column option defaults to :data – create a migration to add a TEXT or JSONB column to your table.

The model requires a set_provider private method that returns an LLM::Provider instance. Legacy set_context and set_tracer convention methods also work for backwards compatibility, but the block style (agent.tracer ..., agent.stream ...) is preferred for all agent-level configuration.

Sequel

Overview

Sequel models use plugin :agent to install the agent wrapper. The plugin accepts the same options as acts_as_agent and follows the same conventions for provider resolution and state persistence. On first call, a fresh agent is created and the conversation starts from scratch. On subsequent calls, the stored state is restored and the conversation continues automatically.

How it works

When you want to add agent persistence to a Sequel model, register the plugin in the model class. The data column stores the full agent state as JSON, same structure as ActiveRecord. On first call, a fresh agent is created. On subsequent calls, the stored state is restored and the conversation continues. Every LLM::Agent#talk or LLM::Agent#ask call persists the updated state.

The data_column: and format: options work identically to ActiveRecord. For :jsonb, Sequel loads the pg_json extension automatically and handles JSON typecasting. The block yields an LLM::Agent instance. Configure agent defaults in the block – model, tools, instructions, tracer, stream, and confirm all go here:

As with ActiveRecord, tracer and stream are configured in the block rather than through legacy convention methods. The block style is the recommended approach.

Migration with TEXT column
DB.create_table :agents do
  primary_key :id
  String :name
  String :data       # stores the serialized agent state
  DateTime :created_at
  DateTime :updated_at
end
Migration with JSONB column
DB.create_table :agents do
  primary_key :id
  String :name
  column :data, :jsonb  # native JSON storage
  DateTime :created_at
  DateTime :updated_at
end
Model setup
require "sequel"
require "llm"
require "llm/sequel/plugin"

class Agent < Sequel::Model
  plugin(:agent, format: :jsonb) do |agent|
    agent.model "deepseek-v4-pro"
    agent.instructions "solve the user's query"
    agent.tools [Research, FinalizeResearch, ActOnResearch]
    agent.tracer LLM::Tracer::Logger.new(llm, io: $stdout)
  end

  private

  def set_provider
    LLM.deepseek(key: ENV["KEY"])
  end
end

agent = Agent.create(name: "researcher")
agent.talk "perform research"
# state is saved to the data column automatically

Why would I use it?

The Sequel plugin integrates with your existing models. It follows the same conventions as ActiveRecord, so switching between the two requires minimal code changes.

Notes

The format option defaults to :string. Use :json or :jsonb for PostgreSQL. Sequel loads the pg_json extension automatically and wraps JSON values for native storage. JSONB is recommended. The data_column option defaults to :data.

The model requires a set_provider private method that returns an LLM::Provider instance. Legacy set_context and set_tracer convention methods also work for backwards compatibility, but configuring tracer, stream, and other agent options in the block is the preferred approach.