diff --git a/docs.json b/docs.json
index 5a3d4dde0..54821448f 100644
--- a/docs.json
+++ b/docs.json
@@ -4642,6 +4642,13 @@
{
"group": "Migrations",
"pages": [
+ {
+ "group": "Agno v3 Migration",
+ "pages": [
+ "other/v3-migration",
+ "other/v3-changelog"
+ ]
+ },
{
"group": "Agno v2 Migration",
"pages": [
diff --git a/other/v3-changelog.mdx b/other/v3-changelog.mdx
new file mode 100644
index 000000000..7963d3b4e
--- /dev/null
+++ b/other/v3-changelog.mdx
@@ -0,0 +1,361 @@
+---
+title: Agno v3.0 Changelog
+sidebarTitle: Agno v3.0 Changelog
+description: "Full list of storage, parameter, and behavior changes introduced in Agno v3.0."
+---
+
+This release rebuilds the storage layer around a normalized runs table, extends
+per-user isolation across the platform, and makes AgentOS background execution
+durable.
+
+The major changes are:
+
+- Session runs are stored one row per run in a dedicated runs table.
+- `user_id` scoping extends to metrics, schedules, evals, knowledge and vector databases.
+- `background=True` on AgentOS is backed by a durable job queue that survives crashes and deploys.
+- Database migrations run through the built-in `MigrationManager`, with schema versions tracked on every adapter.
+
+## Storage
+
+
+ - Runs are no longer stored as a JSON blob in the sessions table. Each run is
+ a row in the runs table (`agno_runs` by default) with `run_id`,
+ `session_id`, `run_type`, `run_index`, `user_id`, `status` and `run_data`.
+ - Saving a run writes one row instead of rewriting the whole session history.
+ This removes the quadratic write amplification and unbounded row growth of
+ the blob design.
+ - `session.runs` is still populated on read: sessions merge the runs table
+ with any legacy blob, so un-migrated sessions keep working.
+ - New direct accessors: `db.get_run(run_id)` and
+ `db.get_runs(session_id=..., user_id=..., status=..., limit=...)`.
+ - The v2 -> v3 migration preserves the legacy `runs` column as a backup.
+ Reclaim it with `db.cleanup_legacy_runs_column(force=True)` (SQL) or
+ `db.cleanup_legacy_runs_field(force=True)` (document/KV adapters) after
+ verifying the migration — the migration keeps every legacy blob as a backup,
+ so the unforced call refuses by design.
+
+
+
+ - `SqliteDb` and `AsyncSqliteDb` issue `PRAGMA journal_mode=WAL` on connect,
+ replacing SQLite's default DELETE journal (a journal create, double fsync and
+ delete on every commit).
+ - WAL is persistent on the database file and produces `-wal` and `-shm`
+ sidecar files next to it. Copy or back up all three together.
+
+
+
+ - `MigrationManager(db).up()` walks all registered migrations for every table
+ and stamps the resulting schema version.
+ - Schema versions are tracked on every adapter, including the document and
+ key-value stores (MongoDB, Redis, Valkey, Firestore, DynamoDB, SurrealDB,
+ JSON, GCS JSON, in-memory). An unstamped database is treated as pre-v3 and
+ migrated.
+ - Migrations are idempotent and non-destructive. Failures raise and abort
+ before any version stamp is written.
+
+
+## Knowledge
+
+
+ - `Knowledge.add_content` -> `insert()`
+ - `Knowledge.add_content_async` -> `ainsert()`
+ - `Knowledge.add_contents_async` -> `ainsert_many()`
+
+
+
+ - `LanceDb`: `use_tantivy` is removed; passing it now raises a `TypeError`.
+ - On schema-based stores, searching a pre-v3 vector table with a `user_id`
+ raises a `ValueError` directing you to the vector database migration,
+ instead of silently returning empty results. Schemaless stores (Qdrant,
+ Pinecone, Upstash, Chroma, MongoDB, OpenSearch, SurrealDB) treat pre-v3
+ documents as shared.
+
+
+## User Isolation
+
+
+ - `user_id` columns added to the schedules, schedule-runs and evals tables.
+ All user-facing read and write methods accept `user_id`.
+ - Metrics aggregate per user. The unique key changed from
+ `(date, aggregation_period)` to `(user_id, date, aggregation_period)`.
+ Sessions without a `user_id` aggregate into a shared bucket that
+ `get_metrics` maps back to `None`.
+ - Knowledge and vector database contents are scoped per user when isolation
+ is enabled. On schema-based stores, searching a pre-v3 vector table with a
+ `user_id` raises a `ValueError` directing you to the vector database
+ migration; schemaless stores treat pre-v3 documents as shared.
+ - Schedule polling (`claim_due_schedule` / `release_schedule`) stays
+ unscoped so background execution fires across all users; each schedule run
+ records the owner denormalized from its parent schedule.
+
+
+## AgentOS
+
+
+ - `AgentOS(enable_mcp_server=..., mcp_config=...)` is replaced by a single
+ `mcp_server=` parameter, which takes a bool or an `MCPServerConfig`.
+
+
+
+ - Passing `page` without `limit`, or a `page` below 1, now raises a
+ `ValueError` instead of being silently ignored. Pages are 1-indexed.
+
+
+
+ - Accepted `background=True` requests are committed job rows that survive
+ crashes, restarts and deploys. Any replica's worker can claim and execute
+ them.
+ - Concurrency is bounded. Excess submissions wait in `pending` status instead
+ of overloading the process.
+ - Runs can be tailed (`stream=true`), 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.
+ - Background execution requires a `db` on the component and returns a 400
+ without one.
+ - External framework agents (LangGraph, Claude, DSPy, etc.) stream inline
+ when `background=true` is requested; their runs are not resumable.
+
+
+
+ - `secret_key` removed from `JWTMiddleware` and `authorization_config`. Use
+ `verification_keys`, which takes a list of keys.
+
+
+
+ - `GET /models` removed. Model data moved into `GET /config` under
+ `available_models`.
+ - `GET /` returns a minimal landing response linking to `/docs`, `/info` and
+ `/health`.
+ - `GET /info` is the single unauthenticated metadata endpoint.
+
+
+## Agents
+
+
+ - `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`
+ - `reasoning=True` removed. Set `reasoning_model=`
+ explicitly.
+ - `continue_run` / `acontinue_run`: `updated_tools` removed. Pass
+ `requirements` (a list of `RunRequirement` from the paused run output).
+
+
+
+
+ - The experimental culture feature is removed: `enable_agentic_culture`,
+ `add_culture_to_context`, `CulturalKnowledge`, the culture tools and the
+ `agno_culture` table.
+ - Use [Knowledge](/knowledge/overview) for shared cross-user information.
+
+
+## Teams & Workflows
+
+
+ - The `Workflow` constructor no longer accepts positional arguments:
+ `Workflow(name=..., steps=[...])`.
+ - `Team` still accepts `Team([agent_1, agent_2])`; the keyword form
+ `Team(members=[...])` is preferred but not required.
+
+
+
+ - Flat HITL kwargs on `Step`, `Steps`, `Loop`, `Condition` and `Router` are
+ removed: `requires_confirmation`, `confirmation_message`, `on_reject`,
+ `requires_user_input`, `user_input_message`, `user_input_schema`,
+ `requires_output_review`, `output_review_message`,
+ `requires_iteration_review`, `iteration_review_message`, `on_error`,
+ `hitl_max_retries`, `hitl_timeout`, `on_timeout`.
+ - Pass `human_review=HumanReview(...)` instead
+ (import from `agno.workflow.types`). Field names are unchanged except
+ `hitl_max_retries` -> `max_retries` and `hitl_timeout` -> `timeout`.
+
+
+## Tools
+
+
+ - `MCPToolbox`: `auth_tokens` and `auth_headers` removed. Use
+ `auth_token_getters`.
+ - Toolkits have an `id`, used by AgentOS to reference tools stably.
+
+
+
+ - `Workspace` now excludes env files (`.env*`, `*.env`) and conventional
+ credential files — private keys and keystores (`*.pem`, `*.key`, `id_rsa*`),
+ credential directories (`.ssh`, `.aws`, `.kube`), registry and host tokens
+ (`.npmrc`, `.netrc`, `.git-credentials`), credential data files
+ (`credentials.json`, `secrets.yaml`, `service_account*.json`) and Terraform
+ inputs (`*.tfvars`).
+ - An agent that reads one of these today starts getting a refusal. Re-allow a
+ specific path explicitly:
+
+ ```python
+ Workspace(".", allow_paths=["config/credentials.json"])
+ ```
+
+ - Committed templates go the other way and become readable
+ (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`).
+ - `credentials.*` and `secrets.*` are deliberately absent from the list: they
+ would also refuse ordinary source such as `credentials.py`.
+ - Known limit: a hard link to an excluded file bypasses the boundary. Symlinks
+ are caught.
+
+
+
+ - `MultiMCPTools` is removed, along with its `allow_partial_failure` parameter.
+ Use one `MCPTools` per server.
+ - The flat Google tool modules are removed: `agno.tools.gmail`,
+ `agno.tools.googlesheets`, `agno.tools.googlecalendar`,
+ `agno.tools.google_maps`, `agno.tools.google_drive` and
+ `agno.tools.google_bigquery`. Import from `agno.tools.google.*` instead.
+ - Google toolkits: `creds_path` -> `credentials_path`, `auth_port` ->
+ `oauth_port`.
+ - `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`: the `async_tools` parameter is removed.
+ - `StudioTool` -> `StudioTools`.
+ - `GDriveContextProvider` -> `GoogleDriveContextProvider`.
+
+
+
+ - `DuckDuckGoTools.duckduckgo_search` -> `web_search`, and
+ `DuckDuckGoTools.duckduckgo_news` -> `search_news`. The toolkit now builds
+ on `WebSearchTools`, which supplies both methods.
+ - `FileTools.check_escape` -> `Toolkit._check_path`. `LocalFileSystemTools`
+ keeps its own `check_escape`, which is unaffected.
+ - `BrightDataTools.get_screenshot`: the unused `output_path` parameter is removed.
+ - `PgVector.enable_prefix_matching` is removed. It was a dead helper with no
+ effect on search.
+
+
+## Scheduler
+
+
+ - The schedules table gains eight nullable columns recording where a schedule
+ came from and who last touched it: `managed_by`, `target_type`, `target_id`,
+ `created_by_run_id`, `created_by_session_id`, `updated_by_run_id`,
+ `updated_by_session_id` and `disabled_reason`. `managed_by` and `target_id`
+ are indexed.
+ - The v3.0.0 migration adds the columns and indexes on SQLite and PostgreSQL
+ (sync and async). Existing rows are left as-is with `NULL` provenance; no
+ data is rewritten. MongoDB needs no schema change.
+
+
+
+ - The schedules unique key becomes `(user_id, name)`.
+ - If duplicate schedule names already exist, the v3.0.0 migration **aborts**
+ rather than stamping itself as done. Resolve the duplicates and re-run.
+
+
+
+ - `update_schedule` can only write `name`, `description`, `method`,
+ `endpoint`, `payload`, `cron_expr`, `timezone`, `timeout_seconds`,
+ `max_retries`, `retry_delay_seconds`, `enabled`, `next_run_at` and
+ `disabled_reason`. Any other key raises a `ValueError` naming the rejected
+ columns.
+ - Ownership, provenance and lock state are no longer writable through the
+ generic update path, so a name-keyed upsert cannot repoint who a schedule
+ belongs to or what it targets.
+
+
+## Evals
+
+
+ - `store_result_in_file`: the `eval_id` parameter is renamed to `run_id`.
+ - `{eval_id}` is no longer accepted in `file_path_to_save_results` templates.
+ Use `{run_id}`.
+ - `POST /eval-runs` now returns the id the row was actually stored under
+ (`run_id`), instead of the eval object's `eval_id`. Every eval run gets its
+ own `run_id`.
+ - The eval classes no longer carry `eval_id`; results carry a per-run `run_id`,
+ so re-runs no longer overwrite each other's stored results.
+
+
+## Models
+
+
+ - The `agno.models.metrics` module and its `Metrics` alias are removed. Use
+ `agno.metrics` and `RunMetrics`.
+ - `Model.classify_error` -> `ModelProviderError.classify(error)`.
+
+
+
+ - The `mistralai` v1 compatibility layer is removed. `agno[mistral]` now
+ requires `mistralai>=2.0.0`.
+ - `agno[mistral]` is included in the `models` extra again.
+
+
+
+ - `Cerebras` and `CerebrasOpenAI` default to `gpt-oss-120b`, replacing
+ `llama-4-scout-17b-16e-instruct`.
+ - OpenAI `reasoning_effort`, `reasoning_summary`, `service_tier` and
+ `verbosity` accept the full set of API values (including `none`, `xhigh`,
+ `max`, `scale`, `fast`, `ultrafast`) and any future string. This widens the
+ accepted types; no existing call breaks.
+
+
+## Learning
+
+
+ - `EntityMemoryStore` with `namespace="user"` did not isolate users: the row
+ key carried no user component, so two users recording the same entity name
+ and type shared one row. One user's facts overwrote the other's and leaked
+ into their prompt context.
+ - Row keys under `namespace="user"` now embed a digest of the `user_id`.
+ Global and custom namespaces are unchanged and do not re-key.
+ - Pre-v3 rows are re-keyed by the v3.0.0 migration, not at runtime. Run it
+ with the rest of your migrations, or call
+ `agno.learn.migrations.rekey_user_entity_learnings` directly. The
+ migration's `down()` refuses to reverse the re-key, since the old key
+ collides users by design.
+ - `EntityMemoryStore.delete` / `adelete` take a keyword-only `user_id` and
+ refuse `namespace="user"` deletes without it. `get` / `aget` now require a
+ `user_id` in that namespace instead of returning an arbitrary user's row.
+
+
+
+ - `enable_agentic_memory` and `memory_manager_id` are removed from every Studio
+ create/edit form (sync and async). Studio components declare memory through
+ `LearningMachine` instead: `learning_name` binds a registry-declared machine,
+ or `enable_learning=True` builds the default one.
+ - The `Agent` and `Team` constructor parameters are unchanged, as are
+ `Registry.memory_managers` and `resolve_memory_manager_reference`, so configs
+ stored with the legacy fields keep rehydrating. Only the Studio authoring
+ surface dropped them.
+ - Enabling learning on a component (a learning machine actually configured
+ via `learning_name` / `enable_learning`) clears `enable_agentic_memory` and
+ `memory_manager`: both register a tool named `update_user_memory`, and the
+ legacy one silently shadowed the store's. Setting them to `False`/`""`
+ leaves the legacy pair alone.
+
+
+
+ - `MemoriesConfig` -> `UserMemoryConfig`
+ - `MemoriesStore` -> `UserMemoryStore`
+ - `Decision` -> `DecisionLog`
+
+
+## Packaging
+
+
+ - `agno[postgres]` installed `psycopg-binary` only, which ships the C
+ accelerator but no importable `psycopg` and no engine layer, so
+ `PostgresDb` raised `ModuleNotFoundError: No module named 'sqlalchemy'` on
+ a clean install. The extra now installs `psycopg`, `psycopg-binary` and
+ `sqlalchemy`.
+ - If you worked around this by installing `psycopg` or `sqlalchemy`
+ yourself, you can drop those pins.
+
+
+## Errors
+
+
+ - SQL adapters raised `Table has an invalid schema` with no next step.
+ The error now names the likely cause (a database created by an older Agno
+ version) and points at both fixes: `asyncio.run(MigrationManager(db).up())`
+ or `POST /databases/all/migrate` on AgentOS.
+
diff --git a/other/v3-migration.mdx b/other/v3-migration.mdx
new file mode 100644
index 000000000..362188a50
--- /dev/null
+++ b/other/v3-migration.mdx
@@ -0,0 +1,573 @@
+---
+title: Migrating to Agno v3.0
+sidebarTitle: Agno v3 Migration
+description: 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.
+
+
+ Reference the [v3.0 Changelog](/other/v3-changelog) for the full list of
+ changes.
+
+
+
+ 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.
+
+
+## Installing Agno v3
+
+If you are already using Agno, you can upgrade to v3 by running:
+
+```bash
+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
+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
+```
+
+
+ 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`).
+
+
+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
+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
+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
+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
+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
+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
+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
+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
+agent = Agent(model=OpenAIResponses(id="gpt-5.5"), reasoning=True)
+```
+
+```python v3_reasoning.py
+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
+workflow = Workflow("my-workflow", steps=[...])
+```
+
+```python v3_workflow.py
+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
+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"])
+```
+
+
+ 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.
+
+
+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
+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.
+- 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() -> ._check_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=. 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=, 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.
+````
+
+
+ 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.
+