Skip to main content

usage-rules.md

# ADK Ex Ecto Usage Rules

`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

```elixir
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`:

```elixir
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`:

```elixir
{: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`):

| Prefix | Table | Scope |
|--------|-------|-------|
| (none) | `adk_sessions.state` | Session-local |
| `app:` | `adk_app_states.state` | Cross-session for the app |
| `user:` | `adk_user_states.state` | Cross-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 discarded**`temp:`-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`:

```elixir
# test/test_helper.exs
{:ok, _} = MyApp.TestRepo.start_link()
Ecto.Migrator.up(MyApp.TestRepo, 0, MyApp.TestMigration, log: false)
ExUnit.start()
```

```elixir
# 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
```