> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agno.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating to Agno v3.0

> Guide to migrate your Agno applications from v2 to v3.

If you have questions during your migration, we can help! See [Get Help](/get-help) for more information.

<Tip>
  Refer the [v3.0 Changelog](/other/v3-changelog) for the full list of
  changes.
</Tip>

<Tip>
  Want to migrate automatically? Jump to [Migrate with AI](#migrate-with-a-coding-agent) for
  a prompt you can paste into Claude, Cursor or any coding agent.
</Tip>

## Installing Agno v3

If you are already using Agno, you can upgrade to v3 by running:

```bash theme={null}
pip install -U agno
```

## Migrating your Agno DB

The built-in migration makes two schema changes:

1. **Session runs move to their own table.** In v2, every session row held its
   full run history as a single JSON blob in the `runs` column. In v3, each run
   is its own row in a dedicated runs table (`agno_runs` by default), which
   removes the write amplification and unbounded row growth of the blob design.
2. **On the SQL adapters, a `user_id` column (with index) is added** to the
   evals, components, knowledge, schedules, schedule-runs and metrics tables, for
   [user isolation](#6-user-isolation-user_id-across-the-platform). The metrics
   unique key changes from `(date, aggregation_period)` to include `user_id`.
   Document and KV backends need no schema change here: per-user scoping on those
   comes from the v3 write path, so on them the migration only moves the runs.

One command applies both:

```python migrate_to_v3.py theme={null}
import asyncio

from agno.db.postgres import PostgresDb  # or SqliteDb, MongoDb, RedisDb, ...
from agno.db.migrations.manager import MigrationManager

db = PostgresDb(db_url="postgresql+psycopg://...")

# Step 1: run all v3 migrations (runs table + user_id columns)
asyncio.run(MigrationManager(db).up())

# Step 2: VERIFY the runs actually landed before any cleanup
runs = db.get_runs(limit=5)
assert len(runs) > 0, "Migration copied nothing - do NOT run cleanup"

# Step 3 (optional, after verifying): reclaim the legacy blob storage.
# The migration deliberately PRESERVES the legacy column as a backup, so the
# rows still hold it and the unforced call refuses. Pass force=True once you
# have verified step 2 -- that is what makes this destructive step explicit.
db.cleanup_legacy_runs_column(force=True)   # SQL adapters
# db.cleanup_legacy_runs_field(force=True)  # Mongo / Redis / Valkey / Firestore / Dynamo / SurrealDB / JSON adapters
```

<Note>
  On the async adapters (`AsyncPostgresDb`, `AsyncMySQLDb`, `AsyncSqliteDb`,
  `AsyncMongoDb`) `get_runs` and the cleanup method are coroutines — await them:
  `runs = asyncio.run(db.get_runs(limit=5))` and
  `asyncio.run(db.cleanup_legacy_runs_column(force=True))` (on `AsyncMongoDb`
  the method is `cleanup_legacy_runs_field`).
</Note>

Vector databases are migrated separately. If you use per-user knowledge with a
vector table created before v3, run the matching script from
[`libs/agno/migrations/v2_to_v3`](https://github.com/agno-agi/agno/tree/main/libs/agno/migrations/v2_to_v3)
(`migrate_sql_vectordbs.py`, `migrate_field_vectordbs.py` or
`migrate_sentinel_vectordbs.py`, depending on your vector store) to add
`user_id` scoping to existing collections. On the schema-based stores (PgVector,
SingleStore, LanceDB, Milvus, ClickHouse, Redis, Cassandra, Couchbase) an
un-migrated table raises a `ValueError` on user-scoped searches instead of
returning empty results. Schemaless stores (Qdrant, Pinecone, Upstash, Chroma,
MongoDB, OpenSearch, SurrealDB) need no migration: pre-v3 documents stay
visible to every user as shared.

Notes:

* The migration is **non-destructive and idempotent**: the legacy `runs` column
  is preserved as a backup, and re-running the migration never duplicates runs.
* Reads keep working before, during and after the migration. Sessions merge the
  runs table with any legacy blob, so an un-migrated session still shows its
  history.
* `cleanup_legacy_runs_column()` refuses to run while legacy data is present
  unless you pass `force=True`. **Only pass `force=True` after Step 2 passes.**
  Cleanup permanently deletes the blob, which is the only copy of your history
  if the migration did not actually copy it.
* Supported everywhere sessions are stored: Postgres, MySQL, SQLite,
  SingleStore, MongoDB, Redis, Valkey, Firestore, DynamoDB, SurrealDB, JSON,
  and GCS JSON, plus the async Postgres, MySQL, SQLite and MongoDB adapters.

For the full storage design and per-adapter details, see the
[v3 storage migration guide](https://github.com/agno-agi/agno/blob/main/libs/agno/agno/db/migrations/V3_MIGRATION_GUIDE.md)
in the repository.

## Migrating your Agno code

Each section covers one breaking change, with before and after examples.

### 1. Sessions and runs (denormalization)

Reading sessions is unchanged. `session.runs` is still populated, now from the
runs table:

```python v3_sessions.py theme={null}
session = agent.get_session(session_id="s1")
session.runs  # still works, loaded from the runs table

# New: fetch runs directly, without loading the whole session
runs = db.get_runs(session_id="s1")
run = db.get_run(run_id="...")
```

If you queried the `runs` column of the sessions table directly (SQL, dashboards,
exports), point those queries at the runs table instead. After cleanup the
column no longer exists:

```sql theme={null}
SELECT run_id, run_data FROM agno_runs WHERE session_id = 's1' ORDER BY run_index;
```

### 2. Workflow HITL: flat kwargs → `HumanReview`

Workflow primitives no longer accept flat HITL kwargs. All human-in-the-loop
configuration lives in one `HumanReview` object.

This is how it looked in v2:

```python v2_hitl.py theme={null}
from agno.workflow.step import Step

step = Step(
    name="deploy",
    executor=deploy,
    requires_confirmation=True,
    confirmation_message="Deploy to production?",
)
```

This is how it looks in v3:

```python v3_hitl.py theme={null}
from agno.workflow.step import Step
from agno.workflow.types import HumanReview

step = Step(
    name="deploy",
    executor=deploy,
    human_review=HumanReview(
        requires_confirmation=True,
        confirmation_message="Deploy to production?",
    ),
)
```

Field mapping: every flat kwarg keeps its name inside `HumanReview`, except
`hitl_max_retries` → `max_retries` and `hitl_timeout` → `timeout`. This applies
to `Step`, `Steps`, `Loop`, `Condition` and `Router`.

### 3. Removed and renamed parameters

These deprecated parameters have been removed. Update them to their v3 names:

**`Agent` and `Team` constructors:**

| v2 (removed)             | v3                                |
| ------------------------ | --------------------------------- |
| `enable_user_memories`   | `update_memory_on_run`            |
| `search_session_history` | `search_past_sessions`            |
| `num_history_sessions`   | `num_past_sessions_to_search`     |
| `num_past_session_runs`  | `num_past_session_runs_in_search` |

```python v3_agent_params.py theme={null}
agent = Agent(
    update_memory_on_run=True,
    search_past_sessions=True,
    num_past_sessions_to_search=3,
)
```

**`continue_run` / `acontinue_run`:** the `updated_tools` parameter is removed.
Pass `requirements` (a list of `RunRequirement`, available on the paused run
output) instead of a modified `ToolExecution` list:

```python v3_continue_run.py theme={null}
run = agent.run("...")  # pauses for confirmation
for requirement in run.requirements:
    requirement.confirm()
agent.continue_run(run_id=run.run_id, requirements=run.requirements)
```

**JWT middleware and `authorization_config`:** `secret_key` is removed. Use
`verification_keys`, which takes a list:

```python v3_jwt.py theme={null}
JWTMiddleware(verification_keys=["your-key"])  # was: secret_key="your-key"
```

**`MCPToolbox`:** `auth_tokens` and `auth_headers` are removed. Use
`auth_token_getters` (same shape: a mapping of auth source names to token
callables).

### 4. Reasoning requires an explicit model

The `reasoning=True` shortcut has been removed. Pass a native reasoning model
explicitly:

```python v2_reasoning.py theme={null}
agent = Agent(model=OpenAIResponses(id="gpt-5.5"), reasoning=True)
```

```python v3_reasoning.py theme={null}
agent = Agent(
    model=OpenAIResponses(id="gpt-5.5"),
    reasoning_model=OpenAIResponses(id="o4-mini"),
)
```

### 5. The `Workflow` constructor is keyword-only

`Workflow` no longer accepts positional arguments:

```python v2_workflow.py theme={null}
workflow = Workflow("my-workflow", steps=[...])
```

```python v3_workflow.py theme={null}
workflow = Workflow(name="my-workflow", steps=[...])
```

`Team` is unchanged: `Team([agent_1, agent_2])` still works. The keyword form
`Team(members=[...])` is preferred for clarity but is not required.

### 6. User isolation: `user_id` across the platform

With `user_isolation` enabled on AgentOS, data is now scoped per user across
**memories, knowledge, evals, metrics, schedules and vector databases**, in
addition to sessions. What this means for your code and data:

* `user_id` columns were added to the schedules, schedule-runs and evals tables;
  the built-in migration handles this.
* Metrics aggregate **per user**: the unique key changed from
  `(date, aggregation_period)` to `(user_id, date, aggregation_period)`.
  Deployments without isolation see the same single-row-per-date shape as
  before; sessions without a `user_id` aggregate into a shared bucket.
* Vector database collections created before v3 have no per-user scoping. On
  schema-based stores, searching them with a `user_id` raises a `ValueError`
  telling you to run the vector database migration — an un-migrated table fails
  loudly instead of silently returning empty results. On schemaless stores
  (Qdrant, Pinecone, Upstash, Chroma, MongoDB, OpenSearch, SurrealDB) pre-v3
  documents are simply treated as shared.

### 7. Background execution and durable queues

`background=True` on AgentOS is rebuilt around a durable job queue. In v2 it
spawned an unbounded `asyncio.create_task`, and a process death silently lost
every waiting and in-flight run. In v3:

* Accepted requests are **committed rows** that survive crashes, restarts and
  deploys; any replica's worker can execute them.
* Runs are **bounded** by a concurrency cap; excess submissions wait in the
  queue in `pending` status instead of overloading the process.
* Every run can be watched (`stream=true` tails), resumed after a disconnect
  (`/resume`) and cancelled from any replica.
* `Idempotency-Key` headers deduplicate resubmissions.
* Redis is optional **coordination** (live event streams, cross-replica
  cancellation), never truth. A Redis fault degrades the live view; it cannot
  lose or corrupt a run.

Breaking implications: background execution requires a `db` on the agent
(enforced with a 400), run status now transitions `pending → running →
completed` (poll `GET /agents/{id}/runs/{run_id}` for the terminal state), and
external framework agents (LangGraph, Claude, etc.) stream inline, so their
runs are not resumable.

### 8. Culture feature removed

The experimental culture feature (`enable_agentic_culture`,
`add_culture_to_context`, `CulturalKnowledge`, the `agno_culture` table) has
been removed. Remove any references; if you need shared knowledge across users,
use [Knowledge](/knowledge/overview) instead.

### 9. Entity memory is isolated per user

If you use `EntityMemoryStore` with `namespace="user"`, your existing rows are
shared across users and must be re-keyed.

In v2 the row key carried no user component, so two users who recorded an
entity with the same name and type wrote to the same physical row: one user's
facts overwrote the other's and then appeared in their prompt context. In v3
the key embeds a digest of the `user_id`. Global and custom namespaces are
unchanged.

Pre-v3 rows are re-keyed **by the migration, not at runtime** — until you run
it, reads still match the old shared rows. The re-key is part of the v3.0.0
migration, so `MigrationManager(db).up()` (or `POST /databases/all/migrate`)
covers it along with everything else:

```python v3_rekey_entities.py theme={null}
from agno.learn.migrations import rekey_user_entity_learnings

# Only needed if you are not running the full v3.0.0 migration.
# dry_run=True is the default: it reports what would change without writing.
print(rekey_user_entity_learnings(db))

# Apply it once the dry run looks right
result = rekey_user_entity_learnings(db, dry_run=False)
print(result["rekeyed"], result["merged"], result["failed"])
```

<Warning>
  This migration cannot be reversed. `down()` refuses the re-key, because the
  pre-v3 key is shared across users and restoring it would collide the rows
  again. Back up the learnings table before running it.
</Warning>

Reading the report: `rekeyed` moved to the owner's key, and `keyed` was already
correct. `merged` is expected rather than an error — if the upgraded application
wrote to the user-scoped key before the migration ran, the entity exists in two
rows and they are folded together, with the newer row winning. `conflicts` and
`failed` need an operator: resolve them, then re-run the helper.

Rows whose stored content records a different user than their owner column held
two users' data before the fix and cannot be separated. The migration moves
these to the `quarantined_user` namespace instead of deleting them: the content
is preserved and entity memory stops reading it. They remain listed and mutable
through the `/learnings` API for whichever user the owner column names. To
delete them instead — along with every row that has no owner — and let entity
memory re-capture from conversation, pass `purge_unrecoverable=True`.

Two API changes come with it:

* `delete` / `adelete` take a keyword-only `user_id` and refuse
  `namespace="user"` deletes without it. Previously any caller could delete
  another user's entity by name.
* `get` / `aget` require a `user_id` in that namespace instead of returning an
  arbitrary user's row.

### 10. Smaller changes

* **AgentOS metadata routes**: `GET /models` was removed (its data moved into
  `GET /config` under `available_models`), and `GET /` is now a minimal landing
  response. `GET /info` is the single unauthenticated metadata endpoint.
* **Toolkits have an `id`**, used by AgentOS to reference tools stably.
* **Schedule provenance columns**: the schedules table gains eight nullable
  columns (`managed_by`, `target_type`, `target_id`, `created_by_run_id`,
  `created_by_session_id`, `updated_by_run_id`, `updated_by_session_id`,
  `disabled_reason`), added by the v3.0.0 migration on SQLite and PostgreSQL.
  Existing rows keep `NULL` provenance and no data is rewritten, so this needs
  no action beyond running the migration. If you query the schedules table
  directly with `SELECT *`, expect the extra columns.
* **`update_schedule` is restricted to a column allow-list**: it now writes only
  `name`, `description`, `method`, `endpoint`, `payload`, `cron_expr`,
  `timezone`, `timeout_seconds`, `max_retries`, `retry_delay_seconds`,
  `enabled`, `next_run_at` and `disabled_reason`. Passing a provenance column
  raises a `ValueError` instead of silently repointing the row's owner or
  target. `user_id` is not an update field either: it scopes the update to that
  owner, so an update passing the wrong `user_id` matches nothing.
* **Removed toolkit methods**: `DuckDuckGoTools.duckduckgo_search` ->
  `web_search` and `duckduckgo_news` -> `search_news`;
  `FileTools.check_escape` -> `Toolkit._check_path`;
  `PgVector.enable_prefix_matching` removed (dead helper);
  `BrightDataTools.get_screenshot` no longer takes `output_path`.
* **Removed learn aliases**: `MemoriesConfig` -> `UserMemoryConfig`,
  `MemoriesStore` -> `UserMemoryStore`, `Decision` -> `DecisionLog`.
* **Eval result files**: `store_result_in_file`'s `eval_id` parameter is now
  `run_id`, and `{eval_id}` is no longer accepted in `file_path_to_save_results`
  templates -- use `{run_id}`. `POST /eval-runs` returns the id the row was
  stored under.
* **`Workspace` refuses credential files by default**: env files and
  conventional credential paths (`*.pem`, `.ssh`, `.aws`, `credentials.json`,
  `*.tfvars`, ...) are excluded, so an agent that reads one starts getting a
  refusal. Re-allow specific paths with
  `Workspace(".", allow_paths=["config/credentials.json"])`. Committed templates
  such as `.env.example` become readable.
* **Studio memory forms**: `enable_agentic_memory` and `memory_manager_id` are
  gone from the Studio create/edit forms. Use `learning_name` (a registry
  machine) or `enable_learning=True`. The `Agent`/`Team` constructor parameters
  are unchanged, so stored configs keep rehydrating.
* **SQLite uses WAL**: `SqliteDb`/`AsyncSqliteDb` connect in WAL journal mode,
  which creates `-wal` and `-shm` sidecar files next to the database. Copy or
  back up all three together.
* **`MultiMCPTools` removed**: use one `MCPTools` per server. The
  `allow_partial_failure` parameter is gone with it.
* **Knowledge insert API**: `add_content` -> `insert()`, `add_content_async` ->
  `ainsert()`, `add_contents_async` -> `ainsert_many()`.
* **Flat Google tool modules removed**: import from `agno.tools.google.*`
  instead of `agno.tools.gmail`, `agno.tools.googlesheets`,
  `agno.tools.googlecalendar`, `agno.tools.google_maps`,
  `agno.tools.google_drive`, `agno.tools.google_bigquery`. Their parameters
  changed too: `creds_path` -> `credentials_path`, `auth_port` -> `oauth_port`.
* **Other toolkit renames**: `SeltzTools.max_documents` -> `max_results`
  (older `seltz` SDKs still work through a fallback; `seltz>=1.2.0` is needed
  for the `scope`, domain and date filters); `BrandfetchTools` drops `async_tools`;
  `StudioTool` -> `StudioTools`; `GDriveContextProvider` ->
  `GoogleDriveContextProvider`.
* **AgentOS MCP config**: `AgentOS(enable_mcp_server=..., mcp_config=...)` ->
  `mcp_server=` (a bool or `MCPServerConfig`).
* **Removed model APIs**: the `agno.models.metrics` module and its `Metrics`
  alias are gone -- use `agno.metrics` / `RunMetrics`. `Model.classify_error`
  -> `ModelProviderError.classify(error)`.
* **`LanceDb.use_tantivy`** is removed; passing it now raises a `TypeError`.
* **Pagination is validated**: `page` without `limit`, or `page < 1`, now raises
  a `ValueError` instead of being ignored.
* **Schedule names are unique per user**: the unique key becomes
  `(user_id, name)`. If duplicate names already exist, the v3.0.0 migration
  aborts rather than stamping itself done -- resolve the duplicates and re-run.
* **Mistral requires `mistralai>=2.0.0`**: the v1 compatibility layer is gone.
  Upgrade with `pip install -U "agno[mistral]"`.
* **Cerebras default model**: `Cerebras` and `CerebrasOpenAI` now default to
  `gpt-oss-120b` instead of `llama-4-scout-17b-16e-instruct`. Pin the old id
  explicitly if you depend on it.
* **`agno[postgres]` installs a working driver**: the extra previously
  installed `psycopg-binary` only, so `PostgresDb` failed with
  `ModuleNotFoundError: No module named 'sqlalchemy'`. It now pulls `psycopg`
  and `sqlalchemy`; you can drop any manual pins you added to work around it.

## Migrate with a Coding Agent

Paste the prompt below into Claude, Cursor, or any coding agent with access to
your repository. It applies the mechanical changes and flags everything that
needs your judgment.

```markdown Copy this prompt expandable theme={null}
You are migrating a codebase from Agno v2 to Agno v3. Apply the following
changes carefully. Make the mechanical edits directly; for anything marked
JUDGMENT, report it to me instead of guessing.

## 1. Renamed parameters (mechanical)

Rename these constructor parameters wherever Agent(...) or Team(...) is called:
- enable_user_memories        -> update_memory_on_run
- search_session_history      -> search_past_sessions
- num_history_sessions       -> num_past_sessions_to_search
- num_past_session_runs       -> num_past_session_runs_in_search

Rename these too, wherever they appear:
- JWTMiddleware / authorization_config: secret_key="k" -> verification_keys=["k"]
  (note the list wrapping)
- MCPToolbox: auth_tokens= or auth_headers= -> auth_token_getters= (same value)

Rename these imports and modules wherever they appear:
- from agno.tools.gmail / googlesheets / googlecalendar / google_maps /
  google_drive / google_bigquery  -> from agno.tools.google.<module>
- Google toolkit kwargs: creds_path= -> credentials_path=, auth_port= -> oauth_port=
- MultiMCPTools(...)            -> one MCPTools per server (JUDGMENT: report it)
- Knowledge.add_content(        -> .insert(
- Knowledge.add_content_async(  -> .ainsert(
- Knowledge.add_contents_async( -> .ainsert_many(
- StudioTool                    -> StudioTools
- GDriveContextProvider         -> GoogleDriveContextProvider
- from agno.models.metrics import Metrics -> from agno.metrics import RunMetrics
- SeltzTools kwarg max_documents= -> max_results=
- BrandfetchTools: drop any async_tools= argument
- LanceDb: drop any use_tantivy= argument
- AgentOS(enable_mcp_server=X, mcp_config=Y) -> AgentOS(mcp_server=Y or X)

Rename these methods and imports wherever they appear:
- DuckDuckGoTools: .duckduckgo_search(  -> .web_search(
- DuckDuckGoTools: .duckduckgo_news(    -> .search_news(
- FileTools: .check_escape(<path>)      -> ._check_path(<path>, self.base_dir)
  (the v3 helper takes the base dir explicitly; a bare token rename breaks the call.
  Do NOT rename LocalFileSystemTools.check_escape - that one still exists)
- BrightDataTools.get_screenshot(...): drop any output_path= argument
- PgVector: remove any .enable_prefix_matching(...) call (the helper method is gone)
- from agno.learn import MemoriesConfig  -> UserMemoryConfig
- from agno.learn import MemoriesStore   -> UserMemoryStore
- from agno.learn import Decision        -> DecisionLog

## 1b. continue_run updated_tools (JUDGMENT)

Agent/Team continue_run and acontinue_run no longer accept updated_tools
(List[ToolExecution]). The v3 path is requirements=<list of RunRequirement from
the paused run output>. This is a structural change to HITL continue code, not
a rename: find every call site passing updated_tools and report it.

## 2. Workflow HITL config (mechanical)

Step, Steps, Loop, Condition and Router no longer accept flat HITL kwargs.
Collect any of these kwargs from their constructors:
  requires_confirmation, confirmation_message, on_reject, requires_user_input,
  user_input_message, user_input_schema, allow_multiple_selections,
  requires_output_review, output_review_message, requires_iteration_review,
  iteration_review_message, on_error, hitl_max_retries, hitl_timeout, on_timeout
and move them into a single human_review=HumanReview(...) argument
(import: from agno.workflow.types import HumanReview).
Rename while moving: hitl_max_retries -> max_retries, hitl_timeout -> timeout.
All other names are unchanged inside HumanReview.

## 3. Reasoning (JUDGMENT)

Agent(reasoning=True) no longer exists. Comment the argument out with a
`# TODO(agno-v3):` marker so the file stays importable, and report every
occurrence: the fix is to set reasoning_model=<a native reasoning model
instance>, and I need to choose which model.

## 4. Keyword-only Workflow constructor (mechanical)

The Workflow constructor is keyword-only. Convert positional arguments:
  Workflow("wf-id", ...) -> Workflow(id="wf-id", ...)
  (v2's first positional argument was `id`, NOT `name` - converting it to name=
  would silently re-identify the workflow: new auto-generated id, different
  AgentOS routing and database rows)
Team is NOT keyword-only: leave Team([a, b]) alone.

## 4b. Entity memory user isolation (JUDGMENT)

If the code constructs EntityMemoryStore(...) with namespace="user", report it.
The row key changed in v3 and existing rows must be re-keyed by the v3.0.0
migration; the change is not reversible, so I need to confirm it. Also report
any call to that store's delete/adelete or get/aget: they now require a
keyword-only user_id in the "user" namespace.

## 5. Culture feature (JUDGMENT)

The culture feature was removed. Find any use of: enable_agentic_culture,
add_culture_to_context, CulturalKnowledge, update_cultural_knowledge, or
imports from agno.culture. Comment constructor arguments out with a
`# TODO(agno-v3):` marker so files stay importable; leave other usages in
place. Report every occurrence.

## 6. Direct SQL against sessions (JUDGMENT)

Search for SQL, dashboard queries or exports reading the `runs` column of the
agno_sessions table. In v3 runs live in the agno_runs table
(run_id, session_id, run_type, run_index, run_data, ...). Report every hit.

## 7. AgentOS API consumers (JUDGMENT)

If this codebase calls the AgentOS HTTP API: GET /models was removed (use
GET /config -> available_models), and GET / returns a minimal landing payload.
Report any client code using those routes.

## 8. Database migration (do NOT automate the destructive step)

Write (but do not execute) a migration script for me with exactly this shape:

    import asyncio
    from agno.db.migrations.manager import MigrationManager
    # build db exactly as the app does
    asyncio.run(MigrationManager(db).up())
    runs = db.get_runs(limit=5)
    assert len(runs) > 0, "Migration copied nothing - do NOT run cleanup"
    print("Migration verified. Run db.cleanup_legacy_runs_column() manually "
          "once you have confirmed history is intact in the UI.")

Never call cleanup_legacy_runs_column / cleanup_legacy_runs_field yourself,
and never pass force=True on my behalf: cleanup permanently deletes the legacy
run history, and must only happen after the verification assert passes AND I
have confirmed the migrated history looks right.

## Output

When done: list every file you changed with a one-line summary, then a
JUDGMENT section listing every finding from steps 1b, 3, 4b, 5, 6 and 7 that needs my
decision. If the repo pins agno in requirements/pyproject, update it to >=3.0.
```

<Tip>
  The prompt deliberately refuses to run the destructive cleanup step. Keep it
  that way: verify your migrated history in the AgentOS UI before reclaiming
  the legacy storage.
</Tip>
