ADK Ex Ecto Usage Rules

Copy Markdown View Source

adk_ex_ecto provides database-backed session persistence for the Elixir ADK (adk_ex). It implements the ADK.Session.Service behaviour using Ecto, with support for SQLite3 (dev/test) and PostgreSQL (production).

Setup

1. Add Dependencies

def deps do
  [
    {:adk_ex, "~> 0.2"},
    {:adk_ex_ecto, "~> 1.0"},
    {:ecto_sqlite3, "~> 0.17"}   # or {:postgrex, "~> 0.19"} for PostgreSQL
  ]
end

2. Create the Migration

In a new Ecto migration file, delegate to ADKExEcto.Migration.up/0:

defmodule MyApp.Repo.Migrations.CreateADKTables do
  use Ecto.Migration

  def change do
    ADKExEcto.Migration.up()
  end
end

Run mix ecto.migrate. This creates 4 tables: adk_sessions, adk_events, adk_app_states, adk_user_states.

3. Wire Up the Runner

Pass your Ecto Repo as session_service and ADKExEcto.SessionService as session_module:

{:ok, runner} = ADK.Runner.new(
  app_name: "my_app",
  root_agent: agent,
  session_service: MyApp.Repo,
  session_module: ADKExEcto.SessionService
)

The Repo module plays the role of the GenServer.server() argument in the ADK.Session.Service behaviour — no extra process is started.

State Routing

State keys are routed by prefix (matching ADK.Session.InMemory):

PrefixTableScope
(none)adk_sessions.stateSession-local
app:adk_app_states.stateCross-session for the app
user:adk_user_states.stateCross-session for the user
temp:(not stored)Current invocation only — discarded on append

On read, get/3 merges app/user state (prefix re-added) with session state into a single flat map.

Database Schema

Composite primary keys, utc_datetime_usec timestamps:

  • adk_sessions(app_name, user_id, id) + state (map)
  • adk_events(id, app_name, user_id, session_id) + content, actions, metadata
  • adk_app_states(app_name) + state
  • adk_user_states(app_name, user_id) + state

Serialization

ADK Content, Part, FunctionCall, FunctionResponse, and Actions structs are JSON-serialized to/from maps automatically. Binary Blob data is Base64-encoded.

Critical Rules

  1. Pass the Repo as session_service, not the SessionService module — e.g. session_service: MyApp.Repo, session_module: ADKExEcto.SessionService. The Repo is the "server"; the SessionService module implements the behaviour callbacks against it.
  2. Call ADKExEcto.Migration.up/0 inside an Ecto migration — don't call it directly at runtime. It wraps create table/2 calls that must run under the migrator.
  3. Partial events are not persisted — events with partial: true are skipped on append_event, matching ADK.Session.InMemory behaviour. Only finalized events are stored.
  4. temp: state is discardedtemp:-prefixed keys in a state delta are dropped during append_event and never written to any table.
  5. Staleness is not checked — the Runner may call append_event with an older session snapshot without re-fetching; this service does not reject on stale updated_at.
  6. Don't use the Ecto sandbox with SQLite3 in-memory + pool_size 1 — connections are serialized and the sandbox deadlocks. Clean tables in a setup block instead.
  7. Events are deleted before the session on delete/3 — foreign-key-like cleanup is done in application code inside a transaction, since composite keys don't auto-cascade.
  8. create/5 upserts app/user state — passing state with app:/user: prefixes on creation will create-or-update the shared-state rows. Existing shared state for other sessions is preserved.

Testing Pattern

Use SQLite3 in-memory with manual cleanup in setup:

# test/test_helper.exs
{:ok, _} = MyApp.TestRepo.start_link()
Ecto.Migrator.up(MyApp.TestRepo, 0, MyApp.TestMigration, log: false)
ExUnit.start()
# in each test module
setup do
  MyApp.TestRepo.delete_all(ADKExEcto.Schemas.Event)
  MyApp.TestRepo.delete_all(ADKExEcto.Schemas.Session)
  MyApp.TestRepo.delete_all(ADKExEcto.Schemas.AppState)
  MyApp.TestRepo.delete_all(ADKExEcto.Schemas.UserState)
  :ok
end