mirror of
https://github.com/agno-agi/agno.git
synced 2026-10-12 02:10:54 +00:00
+12








4965095de3
## 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]>