Files
+12 4965095de3 feat: v3.0 (#8210)
## Summary

This is the v3.0 release: 109 commits from `feat/v3.0` into `main`,
~1,010 files, +190,764/-25,616.

v3.0 changes how Agno stores a session, how it isolates users, and how
much of a run has to live in the model's context. Session runs move out
of the `agno_sessions.runs` JSON blob into a dedicated `agno_runs`
table, which takes write amplification for a session from O(N²) to O(N)
and removes the DynamoDB/Firestore item-size ceiling on long sessions.
Per-user isolation stops being a session-level idea and reaches evals,
metrics, schedules, components, knowledge content, entity memory and 17
vector databases. Large tool results and media bytes can be offloaded
out of the transcript and out of the database entirely. AgentOS gains a
durable background job queue, a governed Studio component catalog, and
pluggable event streams so a run can be followed from any replica.
Alongside that, roughly 50 deprecated parameters, aliases, modules and
endpoints are removed, and the framework's hot paths got measurably
faster (cold import 233 ms to 147 ms; a 25-turn in-memory conversation
38.6 ms to 12.2 ms).

Every 2.x user must read the Breaking changes and Migration sections
below. **A database migration is required before v3.0 serves traffic.**

Related: #8350, #8245, #8262, #9079, #9340, #9436, #9604, #9322, #8940,
#8354, #9560, #9515, #9554.

Release-readiness fixes found while writing this are split out into
#9712. The breaking-change and migration entries below were verified by
execution against this branch — constructing the removed API to confirm
it raises, importing each replacement, and running the migration
end-to-end — not by reading commit messages.

---

## Highlights

### Runtime

- **Tool results and member answers offload to files instead of bloating
the transcript.** `Agent(offload_tool_results=True)` /
`Team(offload_tool_results=True)` writes any tool result over 16,000
characters to AgentFS and leaves a short envelope (preview, size,
`result_id`) in the message. The agent gets `read_result` /
`search_result` (plus async twins) to fetch the rest. No model call on
the write path, nothing summarized away. Tune with
`ResultStore(threshold_chars=..., ttl_seconds=...)`. (#9436, #9684)
- **Media bytes move out of the database.**
`media_storage=S3MediaStorage(bucket=...)` on an Agent, Team or Workflow
uploads images, audio, video and files to local disk, S3 or GCS before
the run is persisted; the row keeps a small `MediaReference` instead of
base64. A 113 KB JPEG goes from ~151,000 characters in the run row to
2,897. No schema change, no migration. (#9340)
- **One HITL config object for workflow primitives.** Step, Loop,
Router, Condition, Parallel and Steps take
`human_review=HumanReview(...)` — pre-execution confirmation, user input
with a schema, post-execution output review, per-iteration review for
loops, multi-route selection for routers, plus shared `on_reject` /
`on_error` / `max_retries` / `timeout` / `on_timeout`. Conflicting flags
fail at construction. (#8354)
- **Background runs are bounded, resumable and crash-durable.**
`arun(background=True)` no longer spawns an unbounded task: a
per-event-loop semaphore caps concurrent background runs (default 32,
`AGNO_BACKGROUND_MAX_CONCURRENCY`), run-status transitions go through an
atomic generation-fenced patch, and every streamed event carries a
stream-assigned `event_index` so a reconnecting client can replay from
where it left off. (#9079)
- **Measured speedups on every hot path.** Single mocked run 65 µs,
agent construction 4.7 µs, cold import 147 ms (was 233 ms), 25-turn
in-memory conversation 12.2 ms (was 38.6 ms — from a loss against
LangGraph to a 1.8x win). Copy-on-write history messages, incremental
run persistence in `InMemoryDb`, O(1) session lookup, cached event-union
type tuples, and `to_dict` no longer deep-serializing fields it
discards. Reference table in `cookbook/performance/README.md`. (#9689,
#9700, #9678, #9693, #9699, #9707)
- **A reassigned `db` no longer serves a stale cached session.** With
`cache_session=True`, reassigning `.db` used to keep serving and growing
the same cached session across databases. The cache is now tagged with
the db it was loaded from and dropped on mismatch. (#9699)
- **A workflow session returns only its own runs.** A step's agent or
team run shared the workflow's session id and came back as one of the
workflow's top-level runs, producing `KeyError: 'steps'` or a bogus
`WorkflowRunOutput`, which made a paused workflow unresumable. Member
runs are skipped on the session read and stay retrievable via
`db.get_runs(session_id=...)`. (#9634)
- **`from agno.workflow import Workflow` works without fastapi
installed.** Two modules imported `WebSocket` at module level purely for
annotations. (#9696)
- **Workflow event serialization survives cyclic object graphs.**
Workflow HITL over the WebSocket hit `maximum recursion depth exceeded`
and silently dropped the event from every sink, including the `/resume`
replay. (#9641)

### Database

- **Runs get their own table.** Each run is a row in `agno_runs` with
real columns for the fields you filter on (`session_id`, `run_type`,
`agent_id`, `team_id`, `workflow_id`, `user_id`, `parent_run_id`,
`status`, `run_index`) and the payload as one JSON value.
`session.get_messages()`, `get_chat_history()`, `db.get_session()` and
the AgentOS session routes are unchanged — runs are re-attached on read.
(#8350)
- **Direct run APIs on the db.** `db.get_run()`,
`db.get_runs(session_id=..., status=..., limit=..., page=...)`,
`db.upsert_run()`, `db.delete_run()`, `db.delete_runs()` — sync and
async. `db.get_session(runs_limit=N)` makes "the last N runs" an indexed
query instead of a full-history load;
`db.get_sessions(include_runs=False)` skips them entirely. (#8350)
- **A migration you can run in one line.** `MigrationManager(db).up()`
creates the runs store and copies every legacy run across,
non-destructively and idempotently, on 12 sync backends and 4 async
ones. AgentOS also exposes `POST /databases/all/migrate`. Un-migrated
databases still work: reads merge the runs table with the legacy blob by
run_id. (#8350)
- **A stale database says so.** Schema mismatches raise a typed
`MigrationRequiredError` / `SchemaMismatchError` naming both remedies,
and AgentOS carries `error_id: "migration_required_error"` through to
the JSON body so a frontend can offer the migration. (#9669, #9631)
- **Per-user isolation reaches evals, metrics, schedules, knowledge and
components.** Metrics aggregate per user per day rather than one global
row. Unowned eval runs are invisible to a scoped caller; unowned
components and knowledge rows are shared — readable by everyone,
editable only by an admin — so enabling isolation does not 404 every
pre-isolation building block. (#8245, #8262)
- **Warm queries stop paying for introspection.** Table resolutions are
cached on `BaseDb`/`AsyncBaseDb` and invalidated wherever a table
actually changes. A warm Postgres `get_session` issues exactly 1 SQL
statement and 0 introspection queries. (#9623)
- **SqliteDb runs in WAL journal mode**, with `synchronous` left at FULL
so durability is unchanged; falls back gracefully where SQLite cannot
switch. The durable 25-turn benchmark went from 52.3 ms to 42.2 ms.
(#9707)
- **Custom session tables get a correctly foreign-keyed runs table.**
Two db instances with different `session_table` names collided on one
shared `agno_runs` whose FK was locked to whichever created it first;
every run from the other instance violated the FK, was swallowed to a
warning, and `run()` still returned `completed`. When `session_table` is
customized and `runs_table` is not set, the runs table name is now
derived as `{session_table}_runs`; the default session table still gets
`agno_runs`, and an explicit `runs_table` always wins. (#9388)
- **Three new tables:** `agno_runs`, `agno_jobs` (the durable background
queue) and `agno_tool_results` (the offload index). All created
automatically.

### AgentOS and Studio

- **Durable background execution.** With
`AgentOS(queue=QueueConfig(durable=True))` an accepted run is a
committed row that survives a crash, restart or deploy and is executed
by whichever replica claims it. At-most-once by default
(`max_attempts=1`), cancellable while queued, `Idempotency-Key` returns
the existing run instead of enqueueing twice, a full queue answers 429.
(#9079)
- **Queue operations REST surface.** `GET /queue/jobs`, `GET
/queue/jobs/{job_id}`, `POST /queue/jobs/{job_id}/requeue`, `GET
/queue/stats`. When the durable queue is off the prefix answers 503
naming the switch. (#9079, #9504)
- **Cross-replica live run views.** Run events flow through a pluggable
`BaseEventStream`. Default is in-memory; `QueueConfig(redis=...)` wires
distributed cancellation in and Redis Streams events out, so SSE
`/resume` and the workflow WebSocket can replay and follow a run from
any replica. (#9079)
- **Durable HITL continuations across five doors.** Continuing a paused
run is a state transition on the existing ticket (`paused → queued`), so
`run_id` stays stable across arbitrary pause/continue cycles. Inline
sync, inline SSE, durable HTTP, workflow WebSocket and MCP/AG-UI/Slack
all pass one admission gate; every other door refuses with 409, so
cross-door double execution is structurally impossible. (#9079, #9404,
#9392, #9499)
- **Studio 3.0: a governed catalog.** `create_*` writes a DRAFT that
serves nobody until `publish_component` promotes it; a draft can be
previewed as a real recorded run by its owner. Writes carry
compare-and-set guards (typed 409 conflicts), deletes are tombstoned,
components archive and restore, and dependent-tracking refuses to orphan
a team or workflow member. Every write is stamped with the acting
`user_id`. (#9604)
- **StudioTools returns a machine-readable envelope.** ~31 flat tools,
each returning `{ok, status, data, error{code, message, details,
retryable}, warnings}` with stable error codes the model branches on
instead of parsing prose. Workflow steps are a recursive
`WorkflowStepSpec`. A palette policy (`allowed_tools`/`denied_tools`)
keeps runtime-discovered tools resolvable but not buildable. (#9604)
- **Zero-config Studio, and workflows as registry citizens.** Studio
toolkits resolve their db lazily from the registry AgentOS declares
(previously an `__init__` snapshot taken too early, which left every
write answering `db_not_configured` forever). `Registry` gains a
`workflows` bucket so Studio's runner can list and dispatch OS-served
workflows. (#9639, #9692)
- **Governed schedules with provenance.** Schedules gain `user_id`,
`managed_by`, `target_type`/`target_id`, actor stamps and
`disabled_reason`. Archiving a component disables every schedule aimed
at it with a reason. The executor refuses to fire a managed schedule
whose endpoint drifted from its provenance target. (#9604)
- **Media routes.** `GET
/sessions/{session_id}/media/{storage_key:path}` streams bytes or
re-signs a URL with `?redirect=true`; session deletes accept
`delete_media=true`. AgentOS auto-discovers the backend from the first
component that declares one. (#9340)
- **One unauthenticated metadata endpoint.** `GET /` is a minimal
landing payload hidden from OpenAPI, `GET /info` is the single metadata
endpoint, `GET /models` is gone and its data moved into `GET
/config.available_models`. (#7647)
- **RBAC filtering now covers DB-loaded teams and workflows.** `GET
/teams` and `GET /workflows` filtered registry components and then
appended DB-loaded ones below the check, unfiltered. Both now mirror
`GET /agents`, and the three list endpoints page to the end instead of
truncating at 20. (#9505, #9639)
- **Unhandled 5xx responses no longer echo exception text.** Store and
driver failures routinely carry connection strings, SQL fragments and
hostnames; 5xx now returns only the exception type. (#9669)

### Tools

- **CodeMode: a persistent Python kernel as a toolkit.** The model's
Python runs in an IPython kernel that lives as long as the session —
variables, imports, helpers and parsed tool results survive across turns
and are snapshotted to AgentFS so they survive process restarts.
Toolkits passed as `CodeMode(tools=[...])` are bound into the kernel as
awaitable handles instead of appearing in the model's tool list, so a
tool call becomes an `await` the model can loop over and compose without
a transcript round-trip. (#9436, #9684)
- **FinanceTools: one finance toolkit, swappable providers.**
`FinanceTools()` uses Yahoo Finance with no API key;
`FinanceTools(provider=FinancialDatasets())` swaps the data source
without touching the agent. Eleven tools returning normalized
dataclasses; unsupported tools are dropped per provider; custom
providers subclass `FinanceProvider`. (#9606)
- **Every Toolkit has a stable id.** `Toolkit` carries an `id`, derived
deterministically from `name` when not supplied. Because every built-in
toolkit forwards `**kwargs`, any toolkit can be given one:
`Calculator(id="calc-prod")`. (#8724)
- **XAIAuth: SuperGrok sign-in as agent tools**
(`sign_in_with_supergrok`, `check_supergrok_login`) for chat and web UIs
where a terminal device-code flow will not work. (#9616)
- **SchedulerTools becomes per-user and refuses dead targets.** The
owner comes from the run's `user_id`; creating or enabling a schedule
aimed at an archived or draft-only component is refused with a typed
error instead of arming a schedule that can only 404. (#8245)
- **AgentOSTools reports failed schedules correctly.** Redaction fired
on any status code, so a failed schedule was reported as "HTTP 200";
`get_platform_metrics` forced a rescan of the current day on every call;
caller-supplied day windows are now normalized once. (#9242)
- **Tools can declare whether they change external state** via
`Function.has_side_effects`, reported by Studio's tool discovery so a
composing agent can tell a read from a write.

### Models

- **New provider: RampRouter (router.com).**
`RampRouter(id="gpt-5.6-luna")` or the model string
`"ramp:gpt-5.6-luna"` fronts OpenAI, Anthropic, Fireworks and xAI behind
one OpenAI-Responses endpoint. Hand it a candidate list
(`models=["openai:gpt-5-nano", "anthropic:claude-haiku-4-5"]`) and it
picks and fails over between them, with `provider_timeout` and
`timeout_before_headers` controlling failover. (#9626)
- **Sign in to xAI with a SuperGrok subscription, no API key.** A new
`xAIResponses` model authenticates via RFC 8628 device-code login;
`XAITokenManager` (imported from `agno.models.xai.oauth`) stores the
session encrypted on `agno_auth_tokens` and refreshes it silently across
restarts. Tokens are keyed by `user_id`, so several people on one
deployment each spend their own subscription. The existing `xAI` chat
model is untouched. (#9616)
- **Claude works on anthropic 1.0.0.** The SDK's 1.0.0 major dropped
`output_format` from `create()`, dropped `temperature`/`top_p`/`top_k`
from every messages surface, and moved from httpx to httpx2. All three
are fixed for first-party Claude, Bedrock, Vertex and Azure Foundry,
with a byte-identical wire request. Floor raised to `anthropic>=0.77.0`.
(#9686, originally reported as #7111)
- **OpenAI reasoning effort and service tier accept every value the API
takes.** Four open unions in `agno.models.openai.types` name today's
documented values and accept any other string, so the next value OpenAI
ships works on the day it ships, while editors still offer the named
values as completions. Inherited by every model built on
OpenAIChat/OpenAIResponses. (#9658)
- **Claude accepts non-PDF document URLs** (fetched and inlined rather
than sent as a `url` source), and **Moonshot file uploads survive
extension-less filenames**. (#9340)

### Knowledge and Learning

- **Per-user RAG isolation across 17 vector databases.**
`Knowledge.search(user_id="alice")` returns Alice's chunks plus the
shared bucket; `user_id=None` is the admin view. Each backend maps the
owner to its own native primitive: a real column on
pgvector/singlestore/lancedb/clickhouse, an indexed payload/keyword
field on qdrant/opensearch/weaviate/mongo, a TAG on redis/valkey, a
`__shared__` sentinel on milvus/cassandra/couchbase, a
collection-per-user on chroma. (#8245)
- **Agents scope their own knowledge searches to the run's user
automatically** — forwarded to `Knowledge.retrieve()`, to a custom
`knowledge_retriever`, to filter validation and to the
`add_to_knowledge` tool, applied after the caller's `**kwargs` merge so
callers cannot override the run owner. (#8245)
- **Knowledge content rows have an owner** and the AgentOS knowledge API
enforces it: cross-user `GET` returns 404 (existence masked rather than
403'd), modifying shared content returns 403, `DELETE
/knowledge/content` clears only the caller's rows. (#8245)
- **Entity memory is isolated per user.** Two users recording an entity
with the same name and type shared one physical row: the first writer's
facts were replaced by the second's and then appeared in the first
user's reads *and in their rendered prompt context* — a cross-user
prompt-injection channel, not just data bleed. Row keys now embed a
digest of the owner. `get`/`delete` fail-opens were closed in the same
change. (#9322, fixes #9319 and #8334)
- **Eval runs are isolated per user**; cross-user GET/PATCH returns 404,
cross-user DELETE is a no-op. The eval framework itself is unchanged.
(#8262)
- **LearningMachine becomes a named, shareable registry resource.**
Components serialize a reference to the name rather than the machine's
config, which makes learning the single memory surface a Studio-built
agent or team can be given. Store initialization is now serialized by a
per-instance lock and backfills late-bound `model`/`db`/`knowledge`.
(#9695)

---

## Breaking changes

### Agent and Team

| Removed / changed | Replacement | PR |
|---|---|---|
| `Agent(reasoning=True)`, `Team(reasoning=True)` | `reasoning_model=<a
native reasoning model>`, or
`tools=[ReasoningTools(add_instructions=True)]` (imported from
`agno.tools.reasoning`) for manual chain-of-thought | #8940 |
| `reasoning_min_steps=`, `reasoning_max_steps=` and the default
Chain-of-Thought engine | Removed. Native reasoning models handle this
internally; a non-native `reasoning_model` now emits an error event and
produces no reasoning | #8940 |
|
`ReasoningConfig.min_steps/max_steps/tools/tool_call_limit/use_json_mode`
| Removed | #8940 |
| `enable_user_memories=` | `update_memory_on_run=True` | #7834 |
| `search_session_history=` | `search_past_sessions=True` | #7834 |
| `num_history_sessions=` | `num_past_sessions_to_search=` | #7834 |
| `continue_run(updated_tools=[...])`,
`acontinue_run(updated_tools=...)` |
`continue_run(requirements=[RunRequirement(tool_execution=...)])`.
**Silent break:** `updated_tools` is absorbed by `**kwargs`, not
rejected — a run continued with the old kwarg resumes with the tool
still unresolved instead of raising. Grep for it. The AgentOS REST
`/continue` endpoint is unchanged and still accepts `tools` | #7834 |
| `culture_manager=`, `enable_agentic_culture=`,
`update_cultural_knowledge=`, `add_culture_to_context=`,
`Agent.get_culture_knowledge()` / `aget_culture_knowledge()` | Removed,
no replacement (was experimental from introduction) | #9515 |
| `agno.culture` package, `agno.db.schemas.culture.CulturalKnowledge`,
`ModelType.CULTURE_MODEL` | Removed | #9515 |
| `Team(members, "id", model, ...)` positional construction |
Keyword-only after `members`: `Team(members, id=..., model=...)` | #9554
|
| `Workflow("id", name, ...)` positional construction | Fully
keyword-only: `Workflow(id=..., name=...)` | #9554 |
| `Agent.from_dict` / `Team.from_dict` shims for
`search_session_history`, `num_history_sessions`,
`enable_user_memories`, `num_past_session_runs` | Removed. `from_dict`
ignores unknown keys, so old serialized configs load with those settings
at their defaults rather than remapped | #9593 |
| `TeamSession.get_messages()` default skip set | Both `AgentSession`
and `TeamSession` now use `HISTORY_SKIP_STATUSES = [paused, cancelled,
error, regenerated]` — team history additionally skips regenerated runs
| — |

### Workflows

| Removed / changed | Replacement | PR |
|---|---|---|
| Flat HITL kwargs on
`Step`/`Loop`/`Router`/`Condition`/`Parallel`/`Steps`:
`requires_confirmation`, `confirmation_message`, `requires_user_input`,
`user_input_message`, `user_input_schema`, `requires_output_review`,
`output_review_message`, `requires_iteration_review`,
`iteration_review_message`, `on_reject`, `on_error`, `hitl_max_retries`,
`hitl_timeout`, `on_timeout`, `allow_multiple_selections` |
`human_review=HumanReview(requires_confirmation=True,
confirmation_message=..., on_reject=OnReject.skip, ...)` | #8354 |
| Injected `session_state` parameter on custom-function steps,
`Condition` evaluators and `Router` selectors |
`run_context.session_state` (`run_context` is still injected by
signature inspection). CEL string expressions are unaffected | #9560 |
| Components serialized with flat HITL keys | On load the keys are
**dropped** with a warning, not translated — the primitive falls back to
a default `HumanReview()` with no HITL behaviour until re-saved | #8354
|

### Modules, imports and aliases

| Removed / changed | Replacement | PR |
|---|---|---|
| `agno.models.metrics` (the whole module) | `from agno.metrics import
RunMetrics, MessageMetrics, ...` | #9560 |
| `agno.metrics.Metrics` alias | `RunMetrics` | #9560 |
| `agno.context.gdrive.GDriveContextProvider` |
`GoogleDriveContextProvider` | #9560 |
| `agno.learn.MemoriesConfig`, `agno.learn.MemoriesStore`,
`agno.learn.schemas.Decision` | `UserMemoryConfig`, `UserMemoryStore`,
`DecisionLog` | #9593 |
| `agno.utils.models._mistral_compat` (mistralai v1/v2 bridge) |
Removed; `mistralai>=2.0.0` only | #9613 |
| `agno.os.middleware.jwt.INTERNAL_SCHEDULER_USER_ID` |
`agno.os.auth.INTERNAL_SCHEDULER_USER_ID` | — |
| `from agno.vectordb.redis.redisdb import RedisDB`, `from
agno.vectordb.valkey.valkeydb import ValkeyDB` | The classes were
renamed `RedisDB` -> `RedisDb` and `ValkeyDB` -> `ValkeyDb`. The
package-level aliases `agno.vectordb.redis.RedisDB` and
`agno.vectordb.valkey.ValkeyDB` are kept (deprecated) — only the
leaf-module import is a hard break | #9560 |
| `ContextProvider._stream_from_agent()` (subclass hook) |
`_arun_sub_agent()` / `_arun_sub_agent_stream()`, gated by
`stream_sub_agent_events` | #9115 |

### Tools

| Removed / changed | Replacement | PR |
|---|---|---|
| `agno.tools.gmail`, `.googlecalendar`, `.googlesheets`,
`.google_drive`, `.google_maps`, `.google_bigquery` (compat stub
modules) | `agno.tools.google.gmail`, `.calendar`, `.sheets`, `.drive`,
`.maps`, `.bigquery` | #9560 |
| `MultiMCPTools` | Removed — use several `MCPTools` instances | #9560 |
| `StudioTool` (singular alias) | `StudioTools` | #9560 |
| `StudioTools(agents_list=, teams_list=, workflows_list=, agents=,
teams=, workflows=)` and the same on `StudioRunnerTools` |
`include_agents=`, `include_teams=`, `include_workflows=`,
`create_agents=`, `create_teams=`, `create_workflows=`. `versions` also
flips default `False` → `True` | #9604, #9639 |
| `StudioTools.list_agents/list_teams/list_workflows/list_dbs`,
`get_agent/get_team/get_workflow/get_version`,
`delete_agent/delete_team/delete_workflow` and all async twins (22
methods) | `list_components`, `get_component`, `archive_component`,
`restore_component`; lifecycle via `publish_component` /
`set_current_version` / `delete_version` | #9604, #9639 |
| `StudioTools.create_*` published immediately; `create_agent(db_id=)`;
`create_workflow(step_specs=)` / `edit_workflow(step_specs=)` | Creates
write a **draft** — pass `publish=True` or call `publish_component`.
`db_id` → `component_id`; `step_specs` → `steps` | #9604 |
| `Toolkit.__init__` positional order — second positional is now `id`,
previously `tools` | Pass by keyword: `Toolkit(name=..., tools=[...])`.
`Toolkit("my-kit", [fn])` now silently passes a list as the id | #8724 |
| `Knowledge.add_content()`, `add_content_async()`,
`add_contents_async()` | `Knowledge.insert()`, `ainsert()`,
`ainsert_many()` | #9560 |
| `FileTools.check_escape(...)` | `Toolkit._check_path(...)` (internal);
no public replacement. `LocalFileSystemTools.check_escape` is kept — it
is that toolkit's own implementation | #9593 |
| `DuckDuckGoTools.duckduckgo_search` / `.duckduckgo_news` method
aliases | `web_search()` / `search_news()`. The tool names the model
sees were already `web_search` / `search_news`, so agent behaviour is
unchanged — this only affects Python callers | #9593 |
| `BrandfetchTools(async_tools=)` | Removed — async variants run
automatically on `arun()` | #7834 |
| `SeltzTools(max_documents=)` and `search(max_documents=)` |
`max_results` | #9560 |
| `BrightDataTools.get_screenshot(output_path=)` | Removed (the
parameter was documented as unused) | #9593 |
| `GoogleDriveTools(creds_path=, auth_port=)` | `credentials_path=`,
`oauth_port=` | #9560 |
| `GoogleSheetsTools(creds_path=)` | `credentials_path=` | #9560 |
| `MCPToolbox.load_tool/load_toolset(auth_tokens=, auth_headers=)` |
`auth_token_getters={name: callable}` | #7834 |
| `LanceDb(use_tantivy=)` | Removed (already an ignored no-op; LanceDB
uses native FTS) | #9560 |
| `PgVector.enable_prefix_matching()`, `PgVector(auto_upgrade_schema=)`
| Removed | #9593 |
| `ZepAsyncTools` toolkit name `"zep_tools"` (and therefore its
auto-generated id) | `"zep_async_tools"` — anything keyed on the toolkit
name or id must be updated | — |

### Models

| Removed / changed | Replacement | PR |
|---|---|---|
| `Model.classify_error(error)` | `ModelProviderError.classify(error)` |
#9593 |
| `OpenAIResponses.get_request_params(messages, response_format, tools,
tool_choice)` | Gained `run_response: Optional[RunOutput] = None` —
subclass overrides must accept it. `OpenAIChat.get_request_params` is
unchanged | #9626, #9616 |
| Moonshot stored file ids: a bare provider file id in `File.id` was
reused on the next run | Ids are written and recognised only with an
`ms://` prefix; unprefixed ids written by 2.x are ignored and the file
is re-uploaded | #9340 |
| Install floors: `anthropic` (unpinned), `openai` (unpinned),
`mistralai` (unpinned), `redis` (unpinned), `postgres =
["psycopg-binary"]` | `anthropic>=0.77.0`, `openai>=1.106.0`,
`mistralai>=2.0.0`, `redis>=4.5.4`, `postgres = ["sqlalchemy",
"psycopg[binary]"]`, `psycopg = ["psycopg[binary]"]`. New extras:
`code`, `finance`, `s3` | #9686, #9613, #9635 |

No model provider was removed. The only provider-directory change is the
added `ramp/`.

### Database and storage

| Removed / changed | Replacement | PR |
|---|---|---|
| The `runs` JSON column on `agno_sessions` | The `agno_runs` table:
`SELECT run_data FROM agno_runs WHERE session_id = :sid ORDER BY
run_index`. On an upgraded database the legacy column survives as a
frozen backup and is **never updated by writes**, so it stops being a
complete view of history the moment v3.0 writes a run | #8350 |
| `PostgresDb(culture_table=...)` and the same kwarg on every adapter;
`db.upsert_cultural_knowledge()`, `get_cultural_knowledge()`,
`get_all_cultural_knowledge()`, `delete_cultural_knowledge()`,
`clear_cultural_knowledge()` | Removed. Existing `agno_culture` tables
are **orphaned, not dropped** | #9515 |
| `agno_metrics` `UNIQUE (date, aggregation_period)`, no `user_id`
column | `UNIQUE (user_id, date, aggregation_period)` with `user_id TEXT
NOT NULL` (`''` = unowned; NULL would defeat the unique key) | #8245 |
| Entity memory row key `entity_user_{entity_type}_{entity_id}` under
`namespace="user"` |
`entity_user_{sha256(user_id)[:16]}_{entity_type}_{entity_id}`.
**Irreversible** — `down()` refuses this step. Direct SQL against
`agno_learnings` must also expect a `quarantined_user` namespace | #9322
|
| `EntityMemoryStore.delete()` / `.adelete()` took no `user_id` |
`delete(..., *, user_id)` — keyword-only, and a `namespace="user"`
delete without it is refused | #9322 |
| Schema validation raised `ValueError("Table X has an invalid schema")`
| `MigrationRequiredError` / `SchemaMismatchError`, both subclasses of
`AgnoError` and deliberately **not** `ValueError` (several routers map
`ValueError` to 400). `except ValueError` no longer catches it | #9669 |
| `is_valid_table` / `ais_valid_table` returned `False` on any
inspection error | Inspection errors propagate; only genuinely missing
columns report as a pending migration | #9669 |
| `InMemoryDb` allowed two session rows sharing one `session_id` with
different agent/team/workflow ids | Upsert conflicts on `session_id`
alone with an owner guard, mirroring the SQL adapters: same-owner writes
update, other-owner writes return `None`, unowned sessions can be
claimed | #9693 |
| Pagination: passing `page` without `limit`, or `page=0` | Both raise
`ValueError`. Pages are 1-indexed; the old behaviour silently returned
every row | #8350 |
| `VectorDb` abstract signatures: `content_hash_exists`, `insert`,
`async_insert`, `upsert`, `async_upsert`, `search`, `async_search`,
`delete_by_content_id` | Each gained a trailing `user_id: Optional[str]
= None`. Unscoped calls against a 2.x adapter still work; a **scoped**
call raises rather than silently storing in the shared bucket. A
`**kwargs`-only signature is deliberately not accepted as v3-aware |
#8245 |
| `Knowledge._build_content_hash` for owned content | Now leads with a
digest of `content.user_id` when one is set, so two users uploading the
same file get distinct rows. Unowned hashes are unchanged | #8245 |

### AgentOS, REST and remote

| Removed / changed | Replacement | PR |
|---|---|---|
| `GET /models` | `GET /config` → read `available_models` | #7647 |
| `ConfigResponse.available_models: Optional[List[str]]` (user-declared
ids) | `List[Model]` — `[{id, provider}]` derived from the agents and
teams actually in use | #7647 |
| `GET /` returning `{name, id, version}` | `{name, health, info}`, plus
`docs` when the docs UI is enabled (the default), and excluded from the
OpenAPI schema. Metadata moves to `GET /info`, which returns `os_id`,
`name`, `os_version`, `agno_version`, entity counts, `mcp` and
`auth_mode` | #7647 |
| `AgentOSClient.get_models()` |
`AgentOSClient.get_config().available_models` | #7647 |
| `POST /a2a/message/send`, `POST /a2a/message/stream` (the flat routes)
| `POST /a2a/{agents\|teams\|workflows}/{id}/v1/message:send` and
`:stream` — these already existed in 2.x and are now the only form |
#9560 |
| `AgentOS(enable_mcp_server=..., mcp_config=...)` and the
`enable_mcp_server` property | `AgentOS(mcp_server=True)` or
`AgentOS(mcp_server=MCPServerConfig(...))` | #9560 |
| `JWTMiddleware(secret_key=...)` / `AuthMiddleware(secret_key=...)` |
`verification_keys=[...]` | #9560 |
| `RemoteDb(culture_table_name=...)` | Removed | #9515 |
| `BaseRemote.acontinue_run(updated_tools=...)` (drives
`RemoteAgent`/`RemoteTeam`/`RemoteWorkflow`) |
`acontinue_run(requirements=[RunRequirement(...)])` | #7834 |
| Agent/Team config payload fields `reasoning_min_steps`,
`reasoning_max_steps`, `enable_user_memories` on `GET /agents` and `GET
/teams` | Removed; `reasoning` is now derived as `reasoning_model is not
None` | #8940, #7834 |
| `version` form field on `POST /agents/{id}/runs` and `POST
/teams/{id}/runs` was `Optional[str]` | `Optional[int]` — a non-numeric
version string now 422s | #9604 |
| With `user_isolation` on, an authenticated caller carrying no user
identity fell through as an unscoped read | 403 `Authenticated request
is missing a user identity` — fail-closed, on REST and WebSocket alike |
#8245 |
| Unhandled 5xx returned `{"detail": str(exc)}` | `{"detail": "Internal
server error (<ExceptionType>)", "error_id": ..., "error_type": ...}`.
Clients parsing 5xx detail text will break; 4xx exceptions that wrote
their own message are untouched | #9669 |
| `MultiMCPTools` in the AgentOS MCP-lifespan detection | The MRO check
now looks for `MCPTools` only | #9560 |
| Workflow WebSocket wire format | Durable submissions now emit a
leading `{"event":"queued","run_id":...,"session_id":...}` ack frame. A
2.x client that does not tolerate an unknown leading frame needs
updating | #9079 |

---

## Migration

### 1. Run the database migration before v3.0 serves traffic

Back up first, then run this once from your deploy step, **before the
application accepts requests**:

```python
import asyncio
from agno.db.migrations.manager import MigrationManager
from agno.db.postgres import PostgresDb

db = PostgresDb(db_url="postgresql+psycopg://...")
asyncio.run(MigrationManager(db).up())
```

In async code, `await MigrationManager(async_db).up()` directly. It is
idempotent, non-destructive, and migrates every table type in one pass:
it creates `agno_runs` and copies every legacy run across, adds
`user_id` to evals / components / knowledge / schedules / schedule_runs
/ metrics, moves the metrics unique key onto `(user_id, date,
aggregation_period)`, adds the Studio schedule-provenance columns, and
re-keys `namespace="user"` entity-memory learnings.

There is no CLI. AgentOS also exposes `POST /databases/all/migrate` and
`POST /databases/{db_id}/migrate`, but those run with the app already
up, which is too late for the entity-memory step below.

Full guide: `libs/agno/agno/db/migrations/V3_MIGRATION_GUIDE.md` **in
this repository** — it is not included in the published wheel, so `pip
install agno` does not give you a copy.

**What self-migrates and what does not.** Session runs self-migrate:
reads merge the runs table with the legacy blob by run_id (table wins on
conflict) and active sessions move their runs across on first save, so
the run migration is strongly recommended rather than strictly
mandatory. **Entity memory does not.** Until the re-key runs, the store
writes to the new per-user key while reads still match the old row, so a
pre-upgrade entity is split across two rows: reads return the old one,
listings show it twice, and a delete removes the row reads are not
returning. This is the one step that genuinely requires downtime. Re-run
it standalone with `from agno.learn.migrations import
rekey_user_entity_learnings; rekey_user_entity_learnings(db,
dry_run=True)` — it does not consult the schema version. Only
deployments that explicitly set `namespace="user"` are affected; the
default `namespace="global"` does not re-key.

**Reclaiming the legacy column.** `db.cleanup_legacy_runs_column()`
(SQL) / `db.cleanup_legacy_runs_field()` (Mongo, Firestore, Redis,
Valkey, DynamoDB, SurrealDB, JSON, GCS-JSON) refuses while any session
still holds a non-null legacy blob. The migration deliberately copies
rather than nulls, and writes never null it, so on any database that
carried 2.x runs the unforced call will always refuse. After verifying
the migrated data against the original, call it with `force=True`.

**Reverting.**
`asyncio.run(MigrationManager(db).down(target_version="2.5.6"))`
rebuilds the session blobs from the runs table and drops the runs table.
Three caveats: the entity-memory re-key is **not** reversed (restore
from backup instead); reverting evals destroys every `user_id` value;
and the metrics revert refuses outright once per-user rows exist (the
guide ships a Postgres consolidation script to run first). SQLite
reverts need SQLite 3.35+.

### 2. Migrate your vector databases for per-user RAG isolation

See `libs/agno/migrations/v2_to_v3/README.md` and the three scripts
beside it.

- **Mandatory data backfill** (existing vectors are invisible to every
scoped caller until the `"__shared__"` sentinel is written): redis,
couchbase, cassandra — `migrate_sentinel_vectordbs.py`.
- **Schema:** pgvector, singlestore — `migrate_sql_vectordbs.py`.
milvus, lancedb, clickhouse, surrealdb — `migrate_field_vectordbs.py`;
milvus needs both the field and the sentinel backfill, and that one
script does both.
- **No migration needed:** qdrant, upstash, mongodb, pinecone, chromadb,
opensearch, valkey.
- **Weaviate cannot be migrated in place.** The shared-bucket filter
needs `index_null_state=True`, which Weaviate only accepts at collection
creation. Recreate the collection through the v3 adapter and re-ingest.
Do **not** add the `user_id` property by hand — the collection then
reports as migrated while every unscoped query throws `Nullstate must be
indexed to be filterable!`, and since `content_hash_exists` runs that
filter on every ingest, `Knowledge.insert` breaks irreparably.
- **Milvus** needs 2.6+ (`AddCollectionField`); on 2.5.x recreate and
re-ingest. Milvus Lite cannot do isolation at all — it drops scalar
fields on the search read path.
- **Couchbase** needs its FTS index updated as well as the document
backfill; pass `search_index_name` in the migration config, or add
`user_id` to the FTS mapping with `analyzer: "keyword"` manually.

### 3. Rewrite removed API surface

- Rename the memory/session params: `enable_user_memories` →
`update_memory_on_run`, `search_session_history` →
`search_past_sessions`, `num_history_sessions` →
`num_past_sessions_to_search`.
- Replace `continue_run(updated_tools=[...])` with
`continue_run(requirements=[RunRequirement(...)])`.
- Rewrite `Agent(reasoning=True)` as `Agent(reasoning_model=<a native
reasoning model>)`. Native families detected: DeepSeek-R1, Anthropic
thinking, OpenAI o-series, Groq, Ollama, Azure AI Foundry, Gemini,
VertexAI. Anything else emits an error event and produces no reasoning —
use `ReasoningTools` for manual chain-of-thought. Note that
`reasoning_agent` customizes native reasoning; it does not replace it.
- Convert positional `Team` / `Workflow` construction to keywords.
`Team([a, b])` still works; anything after `members` must be a keyword.
- Rewrite every workflow primitive that used flat HITL kwargs to
`human_review=HumanReview(...)`, and **re-save any component that was
serialized before this change** — its HITL config is dropped on load
with a warning.
- Rewrite workflow custom functions, `Condition` evaluators and `Router`
selectors that took `session_state` to accept `run_context: RunContext`.
A parameter with no default now raises a missing-argument `TypeError`; a
parameter **with** a default silently receives the default.
- Remove `culture_table=` from every db constructor before upgrading, or
construction raises `TypeError`. Drop orphaned `agno_culture` tables
yourself if you want the space back.
- Move the Google toolkit imports to `agno.tools.google.*`; rewrite
`MultiMCPTools` as a list of `MCPTools`.
- Update StudioTools call sites: the six constructor renames, drop
`db_id` from `create_agent`, and add `publish=True` (or a
`publish_component` step) anywhere 2.x code relied on `create_*`
publishing immediately. Prompts naming `list_agents` / `get_agent` /
`delete_agent` / `list_dbs` must move to `list_components` /
`get_component` / `archive_component` / `restore_component`.
- Move AgentOS clients off `GET /models` and `GET /` metadata; move A2A
clients to the entity-scoped routes; replace `enable_mcp_server=` /
`mcp_config=` with `mcp_server=`, and `secret_key=` with
`verification_keys=`.
- Anything catching `ValueError` around a database call to detect a
stale schema must catch `agno.exceptions.SchemaMismatchError` /
`MigrationRequiredError`.
- Custom `VectorDb` subclasses: add `user_id: Optional[str] = None` to
the eight abstract methods. Custom `Knowledge` implementations used with
`Agent(update_knowledge=True)` must accept `user_id` (or `**kwargs`) on
their insert method, or the `add_to_knowledge` tool returns an error
rather than storing into the shared bucket.
- Subclasses of `OpenAIResponses` / `OpenResponses` overriding
`get_request_params` must add `run_response: Optional[RunOutput] =
None`.

### 4. Enabling `user_isolation` on an existing deployment

There is no backfill or claim step. Stamp `user_id` onto pre-isolation
eval runs, components, knowledge rows, learnings and service accounts
**before** turning isolation on. Unowned components and knowledge stay
readable and runnable by everyone but editable only by an admin; unowned
eval rows become invisible to a scoped caller; a scoped write to an
unowned row is refused (403 for knowledge, learnings and service
accounts).

One metrics row is deleted as the `user_id` column lands — the unowned
`completed = false` row with the newest date, and only when that date
sits past every completed day. The recalculation rebuilds that day from
its sessions, so nothing is lost **unless you prune sessions before
migrating**. Migrate first, prune after.

### 5. Reinstall extras

`agno[postgres]` now pulls `sqlalchemy` + `psycopg[binary]`. Floors
moved: `anthropic>=0.77.0`, `openai>=1.106.0`, `mistralai>=2.0.0`,
`redis>=4.5.4`. `agno[mistral]` is re-enabled inside the `models`
aggregate.

New extras:
- `agno[code]` for CodeMode — needs **Python >= 3.10**; the pins carry
`python_version >= '3.10'` markers so the install resolves on 3.9 but
the import then raises.
- `agno[finance]` for `FinanceTools`' default YFinance provider.
- `agno[s3]` for S3 media storage; `agno[gcs]` for GCS. Local disk needs
nothing.

If you pass a custom `http_client` to any Claude class and install
`anthropic>=1.0.0`, it must now be an httpx2 client — an `httpx.Client`
is dropped with a warning and the SDK default is used instead.

### 6. Optional runtime configuration

- `AGNO_BACKGROUND_MAX_CONCURRENCY` or
`AgentOS(queue=QueueConfig(max_concurrency=...))` tunes the
background-run cap (default 32; 0 or below disables limiting).
- `QueueConfig(durable=True)` requires sync `PostgresDb`,
`AsyncPostgresDb` or `RedisDb`. SQLite raises at startup. Across a
fleet, `lock_grace_seconds`, `stop_timeout_seconds`,
`retention_seconds`, `max_attempts` and `timeout_seconds` must be
configured **uniformly**, including transiently during a rolling deploy.
- Several AgentOS deployments sharing one Redis must set
`RedisCoordination(key_prefix=...)` per deployment or they read each
other's runs by run_id.
- SuperGrok OAuth needs `XAI_TOKEN_ENCRYPTION_KEY` (there is
deliberately no `AGNO_ENCRYPTION_KEY` fallback) and a Postgres or SQLite
db for the auth-tokens table; other backends degrade to a 0600 file or
process memory, which cannot hold per-user sessions.

---

## Type of change

- [x] Bug fix
- [x] New feature
- [x] Breaking change
- [x] Improvement
- [x] Model update
- [x] Other: database schema migration, packaging and dependency floors

---

## Checklist

- [x] Code complies with style guidelines
- [x] Ran format/validation scripts (`./scripts/format.sh` and
`./scripts/validate.sh`) — `format.sh` reports no drift; `validate.sh`
is clean in CI's `.[dev]` environment. Locally it exits 1 with 72 mypy
errors in 14 files that are environment-dependent (SDKs absent from CI
are silenced by `ignore_missing_imports`), and one of those is a real
defect — see Additional Notes.
- [x] Self-review completed
- [ ] Documentation updated (comments, docstrings) —
`V3_MIGRATION_GUIDE.md` is written but does not ship to users; see
Additional Notes.
- [x] Examples and guides: cookbooks added or updated across
`cookbook/02_agents/22_result_offloading/`,
`cookbook/03_teams/27_result_offloading/`,
`cookbook/05_agent_os/{02_databases,07_security,22_studio,background_tasks}/`,
`cookbook/06_storage/`,
`cookbook/07_knowledge/04_advanced/07_per_user_isolation/`,
`cookbook/90_models/{ramp,xai,openai}/`, `cookbook/91_tools/finance/`,
`cookbook/code/`, `cookbook/performance/`
- [ ] Tested in clean environment — the `test-clean` / `test-clean-os`
jobs currently install `agno` from PyPI, which resolves to **2.9.0**,
not this branch. They assert nothing about v3.0. See Additional Notes.
- [x] Tests added/updated (if applicable) — new suites for the
runs-table migration, user isolation across 17 vector DBs, entity-memory
re-key, table-resolution caching, keyword-only constructors, CodeMode,
FinanceTools, RampRouter, xAI OAuth and Claude request params

### Duplicate and AI-Generated PR Check

- [x] I have searched existing [open pull requests](../../pulls) and
confirmed that no other PR already addresses this issue
- [ ] If a similar PR exists, I have explained below why this PR is a
better approach
- [x] Check if this PR was entirely AI-generated (by Copilot, Claude
Code, Cursor, etc.) — no; this is the aggregate of 109 reviewed PRs

---

## Additional Notes

### Blocking before merge or tag

1. **Version is `3.0.0a3`.** `libs/agno/pyproject.toml` still says
`3.0.0a3`, and 3.0.0a1/a2/a3 are all already on PyPI. `release.yml`
checks whether `project.version` exists on PyPI and skips the build
**reporting success** if it does — so publishing a `3.0.0` GitHub
Release from this branch today would produce three green jobs, zero
artifacts, and no error. Bump to `3.0.0` before tagging, and consider
adding a guard step that fails when the tag name does not match the
pyproject version.
2. ~~**`agnoctl` must be bumped.**~~ Done in #9712: agnoctl 0.1.3 ->
**0.2.0**, and agno's floor raised to `agnoctl>=0.2.0`. #9600 added ~900
lines and a new hard runtime dependency (`questionary`) under the
already-published 0.1.3, so the release workflow would have skipped it
and every v3.0.0 installer would have got the stale CLI. `agno-infra` is
byte-identical to main and does not need to move.
3. ~~**`libs/agno/tests/integration/db/` (61 files) is run by no CI
job**~~ Fixed in #9712, which adds a `test-db` job and repairs the six
failing tests (all test-side debt; no product bug). Original finding:
**`libs/agno/tests/integration/db/` (61 files) was run by no CI job** —
`test-remaining` explicitly `--ignore`s it, and its service-free SQLite
subset is **red on this head**:
`test_service_accounts.py::test_token_hash_lookup_reraises_on_db_error`
fails ("DID NOT RAISE"). That test pins that a dead DB connection must
raise so the verifier returns 503 rather than misreading it as an
unknown token (401) — an auth fail-open. 12 v3.0 commits touched
`db/sqlite/`, #9623 (table-resolution cache) being the likely cause. 102
tests, 0.81 s, no infrastructure. Triage before merging.
4. **`main` branch protection requires zero status checks.**
`required_status_checks.contexts` and `.checks` are both empty, so none
of the green checks gate this merge; the only enforced gate is one
approving review (`enforce_admins: false`, `allow_force_pushes: true`).
Populate the required checks before merging, at minimum `tests
(1/5)`..`(5/5)`, the four `style-check-*` jobs and `Lint PR Title and
Body`.
5. ~~**`test-clean` / `test-clean-os` certify PyPI agno.**~~ Fixed in
#9712 — both now install the branch. Until that merges, neither gate
says anything about v3.0.
6. **`requests` is imported unguarded but declared in no extra.**
`agno/utils/location.py:3` and `agno/utils/certs.py:3` import `requests`
at module level; it appears in `pyproject.toml` only in the mypy ignore
list. `Agent(add_location_to_context=True)` — a documented public kwarg
— still *constructs* on a core-only install; every run then raises
`ModuleNotFoundError: No module named 'requests'` while building the
system message. Pre-existing in 2.x; it should not ship in 3.0.0.
Replace with `httpx` (already core) or declare the dependency.
(`certs.py` has no in-tree callers, so it is a latent risk only.)
7. ~~**`libs/agno/requirements.txt` pins `agnoctl==0.1.0a4`**~~ Fixed in
#9712 — regenerated, and it now also carries `questionary`,
`prompt-toolkit` and `wcwidth`, which it was missing.
8. **Real bug found by local mypy that CI cannot see:**
`libs/agno/agno/models/aws/claude.py:163` calls
`self.async_client.close()` from a sync `def get_async_client`. `close`
is a coroutine function, so the coroutine is discarded and never awaited
— the Bedrock connection pool is *not* closed, which is exactly what the
comment three lines above claims it prevents. CI's mypy job installs
only `.[dev]`, so `anthropic` is absent and the error is silenced.
9. **The migration guide does not ship.** `V3_MIGRATION_GUIDE.md` (665
lines, the most complete migration document in the release) lives inside
the package tree, but `pyproject.toml` declares `package-data =
["py.typed"]` with no `MANIFEST.in`, so setuptools packages only `.py`
files. Code references the guide by name (`v3_0_0.py:113`) but a `pip
install agno` user never gets it, and there is no cookbook and no docs
page. Publish it to docs.agno.com or add it to package-data before
release. Two content bugs to fix while doing so: the guide's Step 2 and
"How writes work now" claim that writes null the legacy `runs` column —
every adapter deliberately does the opposite (the guide's own Breaking
changes section states this correctly), which means
`cleanup_legacy_runs_column()` without `force=True` can never succeed as
Step 3 presents it; and Breaking changes #3 names only the four
Postgres/SQLite adapters as accepting `runs_table`, when MySQL,
AsyncMySQL, SingleStore and Mongo (`runs_collection`) do too.

### Known limitations shipping in 3.0

- **Three vector DBs accept `user_id` and ignore it:** `langchaindb`,
`llamaindex`, `lightrag` log a warning and return unscoped results.
Isolation covers 17 of 20 backends, not all. Release copy must not claim
otherwise.
- **A dropped `user_id` degrades to the admin view, not to an error.**
Correct for single-user deployments, but any missed plumbing is a silent
full-corpus read rather than a visible failure.
- **Studio's component catalog is sync-only on `SqliteDb` and
`PostgresDb`.** `AsyncBaseDb` raises `NotImplementedError` for every
component method, and AgentOS mounts a 503 `/components` router. AgentOS
on `AsyncPostgresDb` loses the whole catalog.
- **The durable queue works on Postgres and Redis only**; every other
adapter raises at startup. `QueueConfig(durable=True)` is also not a
blanket guarantee: remote/factory/version-pinned components, media
uploads and non-JSON-serializable kwargs fall back to the bounded
in-process path with a logged warning.
- **Tool-result offloading works on Postgres and SQLite only.** On any
other adapter it warns once and runs with offloading off. The warning is
once-per-owner, so it is easy to miss in production.
- **`ClickhouseDb` has no v3.0.0 migration branch and still stores runs
inline** (traces/spans only); `InMemoryDb` also stores runs inline. The
O(N²) → O(N) fix is not universal.
- **SingleStore's runs table cannot enforce `run_id` uniqueness**
(`PRIMARY KEY (run_id, session_id)` with `SHARD KEY (session_id)`):
`get_run` returns one row unordered, `upsert_run` inserts instead of
updating, `delete_run` removes every copy across sessions. It also keeps
no unique constraint on metrics, so two concurrent refreshes can
double-write a bucket.
- **Schedules exist on Postgres, SQLite and Mongo only**, yet the
migration adds `user_id` to `schedules`/`schedule_runs` across all seven
SQL adapters.
- **Learnings REST scope gap carried forward from #9322:** the API
filters on the owner column rather than the namespace, so a
`quarantined_user` row is still listed, patched and deleted by whichever
user the owner column names, and `DELETE /learnings/users/{other}` does
not reach it. "Quarantined" is scoped to the entity store, not absolute.
Re-run any pre-fix erasure requests after migrating.
- **`enable_agentic_memory` and a `LearningMachine` with a user-memory
store still conflict outside Studio.** Both register a tool named
`update_user_memory`; tool parsing keeps the first, and the learning
store's tool is dropped silently. Studio's create/edit path clears the
legacy pair; a code-defined `Agent(enable_agentic_memory=True,
learning=...)` is not guarded.
- **CodeMode is not a sandbox.** It runs arbitrary Python and shell with
the permissions of the agent process, and snapshot restore is `dill`
unpickling, so the snapshot store inherits the trust level of the
database holding it. This belongs in the docs prominently, not only in a
docstring.
- **`checkpoint="tools"` still raises `NotImplementedError('... reserved
for the 3.0 runs-table split and not available yet')`** — on the 3.0
branch, after the split landed. Either finish it or reword the message
and docstring.
- **The one benchmark Agno still loses:** the 25-turn durable (SQLite)
conversation at 42.2 ms vs LangGraph's 36.5 ms (0.9x), documented in
`cookbook/performance/README.md`. Remaining gap is per-turn
serialization of session state that grows with conversation length.
- **16 of 21 provider CI suites are statically disabled** (`if: false`).
Cohere, Fireworks, DashScope, Cerebras, IBM watsonx, LangDB, AImlAPI,
SambaNova, Together and NVIDIA also have no unit-level substitute, so
those model classes ship in 3.0 with zero executed tests.
`test-cerebras`'s disable has no stated reason (added in a release-batch
commit, #7494) and should be triaged or documented.
- **Two deferred removals** that will break again in a later release
rather than at 3.0: `LearningMode.HITL` is kept, and standalone SSE as
an MCP transport is kept with its deprecation upgraded from `log_info`
to `log_warning`. Both carry "will be removed in a future release" into
the whole 3.x line.
- **Toolkit parameter naming is inconsistent at 3.0.** #9690 (`enable_X`
→ `X` with back-compat) is still open. New `FinanceTools` uses
unprefixed flags, `GoogleSheetsTools`/`GoogleBigQueryTools` renamed with
`enable_*` kept as aliases, and ~100 other toolkits still take
`enable_*` as the primary name. `FinanceTools` also ships alongside
`YFinanceTools`, `FinancialDatasetsTools` and `OpenBBTools` with no
deprecation on any of the three, so there are four overlapping finance
surfaces and no signal about which is canonical.
- **`Function.has_side_effects` has no ergonomic setter** — it is not a
`@tool` decorator parameter and no in-tree toolkit sets it, so Studio's
discovery reports `None` for every shipped tool. Effectively unused at
release.
- **`agno-infra` overwrites `agno/__init__.py` with an empty file** when
installed after `agno`, breaking `from agno.os import AgentOS` and
flipping the `agno` console script. Order-dependent (a single-shot `pip
install agno[infra]` is fine), reachable via the `infra`, `docker` and
`aws` extras.
- ~~**`libs/agno/agno/table.py` is a committed scratch script**~~
Deleted in #9712. It shipped in the wheel and dropped the `agno_spans`
table on import.

### Follow-ups after 3.0.0 is published

- Regenerate the four cookbook lockfiles still pinning agno 2.x
(`cookbook/00_quickstart`, `cookbook/08_learning`, `cookbook/01_demo`,
`cookbook/data_labeling/image_search`). This must wait until a stable
tag exists, since `uv pip compile` will not resolve to a pre-release.
- Recover the Tier 4 (silent deprecations needing a warning cycle) and
Tier 5 (data-migration-gated, e.g. the Qdrant unnamed-vector layer)
lists from #9560's PR body — the audit document that drove them was
deleted from the tree by `c2a25f9bc`.
- `AgentOSConfig.available_models: List[str]` still exists but nothing
reads it after #7647 — dead configuration a user can set with no effect.
- #9667 (wake the local queue worker on enqueue) is still open, so
durable submissions wait for the worker's next poll tick.
- Re-verify `git rev-list --count $(git merge-base origin/main
origin/feat/v3.0)..origin/main == 0` immediately before merging. It is 0
today, which is the only reason the push-triggered integration matrix
and the pull_request merge-ref agree; `test_on_release.yml` has no
`pull_request` trigger, so anything landing on main first leaves the
merged state unverified.

---------

Co-authored-by: Sannya Singal <[email protected]>
Co-authored-by: sannya-singal <[email protected]>
Co-authored-by: Harsh <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: Yash Pratap Solanky <[email protected]>
Co-authored-by: ysolanky <[email protected]>
Co-authored-by: Mustafa Esoofally <[email protected]>
Co-authored-by: Harshita Jain <[email protected]>
Co-authored-by: Ashpreet <[email protected]>
Co-authored-by: Harsh Sinha <[email protected]>
Co-authored-by: Monalisha Mishra <[email protected]>
Co-authored-by: Monalisha Mishra <[email protected]>
Co-authored-by: Harsh Sinha <[email protected]>
Co-authored-by: Ayush Jha <[email protected]>
Co-authored-by: Himanshu singh <[email protected]>
Co-authored-by: Ashpreet <[email protected]>
Co-authored-by: Ashpreet <[email protected]>
Co-authored-by: Rodrigo Casagrande <[email protected]>
Co-authored-by: Anurag <[email protected]>
Co-authored-by: Samuel Jupe <[email protected]>
Co-authored-by: Ashpreet <[email protected]>
2026-08-24 18:19:42 +05:30
..
2026-08-24 18:19:42 +05:30
2026-08-24 18:19:42 +05:30
2026-08-24 18:19:42 +05:30
2026-08-24 18:19:42 +05:30

CodeMode

CodeMode gives the agent one programmable environment instead of a wide tool schema. The model writes Python; the code runs in an IPython kernel that lives as long as the session. Variables, imports, helper functions, and parsed tool results survive across turns. Every other toolkit you attach becomes a callable inside that kernel rather than a separate entry in the model's tool list, so a tool call is an await expression the model can bind to a variable, loop, and compose.

CodeMode caps what a cell can print, so the context holds conclusions and the kernel holds the data. Its sibling for ordinary tools is result offloading, Agent(offload_tool_results=True), in ../02_agents/22_result_offloading/.

Start with 01_basics/basic.py.

Install

CodeMode needs an optional extra:

pip install 'agno[code]'

Layout

cookbook/code/
├── README.md
├── 01_basics/                 # one tool, a live kernel
├── 02_tools_in_code/          # toolkits as awaitable handles
└── 03_persistence/            # state that survives the process

Folders

  • 01_basics/: the smallest working agent, and %%bash shell cells.
  • 02_tools_in_code/: binding a toolkit into the kernel, and composing with the agent filesystem.
  • 03_persistence/: dill snapshots into AgentFS, and the developer-facing surface (run / variables / value / shutdown).

Security

execute runs arbitrary Python and arbitrary shell with the permissions of the process running the agent. It is not a sandbox and does not pretend to be one: use a trusted operator or an isolated container. allow_shell=False strips the %%bash magic, but that is a footgun reducer, not a boundary.

One consequence deserves its own sentence: restore is also code execution. A snapshot is a dill pickle, unpickling runs __reduce__, and restore happens automatically on resume — before any model call. A writable snapshot row is therefore remote code execution in the agent's process. The snapshot store inherits the database's trust level exactly. For the same reason, never point another writer at the FileSystem namespace CodeMode snapshots into - a FileSystemTools that can write kernel/<session>/vars/*.b64 there lets whatever drives it plant a payload the next resume unpickles.

A session's environment belongs to the user of the run that created it, in memory and in the snapshot. A run that names the same session_id with a different user_id is refused and gets neither the warm kernel nor the stored variables. Runs without a user_id carry no identity to compare and keep sharing a session by id; such a run takes the recorded owner rather than clearing it, so a later run by a different user is still refused. Identity is compared as text, so 42 and "42" are the same user.