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. +