Human approval before risky agent tool calls
Originally published on hivegate.dev.
Letting an agent search an inbox is low risk. Letting it send an email, cancel a meeting or permanently delete a message is not. A wrong recipient or a hallucinated date becomes a real-world mistake. The usual answer is human-in-the-loop: the agent proposes the call, a person reviews it, and only then does it run.
This post shows how that works in HiveGate, an open-source FastAPI service for Agno agents. The run pauses, your app gets the proposed call, and a second request approves, edits or denies it.
Which tools pause
Agno lets a toolkit mark tools as requires_confirmation. When the model calls one, Agno stops the run before executing it. HiveGate's built-in toolkits mark every write:
- Email: send_email, create_draft, send_draft, trash_email, delete_email_permanently, modify_labels
- Calendar: schedule_meeting, schedule_meeting_find_time, cancel_meeting
- Contacts: create_contact, update_contact, delete_contact
- Drive: read_file
- Workers: run_claude_code, run_managed_agent
Read-only tools such as search_emails and list_events run without a pause. Your own Agno toolkits opt in the same way, through requires_confirmation_tools=[...]. Tools from MCP servers don't have an approval gate yet. That's on the roadmap.
Step 1: the run pauses
A normal chat request:
curl -X POST http://localhost:8000/v2/agents/support-agent/chat \
-H "X-API-Key: $HIVEGATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Email jordan@acme.example that the renewal call moves to Thursday 3pm.",
"user_id": "user-1842",
"session_id": "inbox-1",
"timezone": "America/Chicago",
"locale": "en-US",
"stream": false,
"user_profile": {
"profile_id": "user-1842",
"email": "dana@acme.example",
"full_name": "Dana Lee",
"role": "Account manager",
"tenant_id": "acme"
}
}'
Instead of a finished answer, the response has status: "PAUSED", a run_id, and the pending call with its arguments (trimmed here):
{
"agent_id": "support-agent",
"session_id": "inbox-1",
"status": "PAUSED",
"run_id": "6f1c9a2e-...",
"tools": [
{
"tool_call_id": "call_8Jx2...",
"tool_name": "send_email",
"requires_confirmation": true,
"tool_args": {
"to": ["jordan@acme.example"],
"subject": "Renewal call moved to Thursday",
"body_text": "Hi Jordan, the renewal call is now Thursday at 3pm CT. Thanks, Dana"
},
"result": null
}
]
}
Nothing has been sent yet. With "stream": true, you get the same data as an SSE event whose event field is RunPaused, and then the stream ends.
Show tool_name and tool_args to the user, or to whoever reviews actions for them, in whatever form fits your app: a confirmation card, an editable form, a Slack message.
Step 2: approve, edit or deny
The decision goes to POST /v2/agents/{agent_id}/chat/commit with the run_id, the same user_id and session_id, and one entry per pending tool in updated_tools.
Approve as proposed:
curl -X POST http://localhost:8000/v2/agents/support-agent/chat/commit \
-H "X-API-Key: $HIVEGATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"run_id": "6f1c9a2e-...",
"user_id": "user-1842",
"session_id": "inbox-1",
"stream": false,
"updated_tools": [
{"tool_call_id": "call_8Jx2...", "confirmed": true}
]
}'
The agent resumes, the email is sent through the user's own Gmail or Outlook account, and the response has the agent's final message, status: "completed" and the token usage for the resumed part.
Edit, then approve. The reviewer fixed the time, so send the corrected arguments with the approval. tool_args replaces the proposed arguments, so send the full set:
{"tool_call_id": "call_8Jx2...", "confirmed": true,
"tool_args": {
"to": ["jordan@acme.example"],
"subject": "Renewal call moved to Thursday",
"body_text": "Hi Jordan, the renewal call is now Thursday at 4pm CT. Thanks, Dana"
},
"confirmation_note": "Corrected time to 4pm"}
Deny:
{"tool_call_id": "call_8Jx2...", "confirmed": false, "confirmation_note": "Wrong recipient"}
If every pending tool is denied, HiveGate doesn't resume the run at all. It returns status: "cancelled" with the content "Tool execution cancelled by user.", and no model call is made. If you approve some tools and deny others, the run resumes: Agno executes the approved ones, skips the denied ones, and tells the model they were rejected.
The fields HiveGate accepts in each updated_tools entry are confirmed, tool_args (or args) and confirmation_note. Anything else is ignored.
The same thing from Python
A minimal backend helper, with the review step left to your UI:
import httpx
BASE = "http://localhost:8000/v2/agents/support-agent"
HEADERS = {"X-API-Key": HIVEGATE_KEY}
def chat(message: str, user: dict, session_id: str) -> dict:
body = {
"message": message,
"user_id": user["profile_id"],
"session_id": session_id,
"timezone": "UTC",
"locale": "en",
"stream": False,
"user_profile": user,
}
return httpx.post(f"{BASE}/chat", headers=HEADERS, json=body, timeout=120).json()
def decide(paused: dict, user: dict, decisions: list[dict]) -> dict:
"""decisions: one {"tool_call_id", "confirmed", optional "tool_args", "confirmation_note"} per pending tool."""
body = {
"run_id": paused["run_id"],
"user_id": user["profile_id"],
"session_id": paused["session_id"],
"stream": False,
"updated_tools": decisions,
}
response = httpx.post(f"{BASE}/chat/commit", headers=HEADERS, json=body, timeout=120)
if response.status_code == 410:
raise RuntimeError("Paused run is gone; ask the agent again.")
response.raise_for_status()
return response.json()
reply = chat("Cancel my 2pm with Jordan.", user, "inbox-1")
if reply["status"] == "PAUSED":
pending = [t for t in reply["tools"] if t.get("requires_confirmation")]
decisions = review_in_ui(pending) # your code: returns approve/edit/deny per tool
reply = decide(reply, user, decisions)
print(reply["content"])
If the chat request named a model, send the same model on commit. HiveGate uses it to find the paused agent.
Where paused runs live
A paused run is held in the memory of the HiveGate process that paused it, until it's committed. If the process restarts, or the commit lands on a different replica, commit returns 410 Gone ("Confirmation time window has elapsed"). Two practical consequences:
- Run a single replica, or route each session to the same instance, when you use approvals.
- Treat a 410 as "the proposal expired": ask the agent again rather than retrying the commit.
The conversation itself is stored in Postgres, so the user doesn't lose their history when a pending action expires.
The audit trail
Here is what HiveGate records about a decision:
- The run, in the agent's session. Agno stores each run in the agent's Postgres session table. A resumed run's tool executions carry the arguments that actually ran (including any edits), requires_confirmation, confirmed and the confirmation_note.
- The cost of the resumed run. The resumed part is metered like any other call: a row in token_usage with the user, session, API key name, number of tool calls, status and USD cost. You can list them with GET /usage/calls?agent=support-agent&order=recent, which needs a key with the usage:read scope.
- Logs. Every commit is logged with its run_id, and a fully denied run is logged as such. Set OTEL_TRACING_BACKEND=otlp and OTEL_LOGGING_BACKEND=otlp (Sentry and Logtail are also supported) to send traces and logs to your observability stack.
What it doesn't record is which person clicked approve. HiveGate sees the API key that made the commit, not your end user's identity. If you need that for compliance, record the reviewer and the decision in your own database before you call commit. You can also put the reviewer in confirmation_note, which is stored with the tool execution. A fully denied run is never resumed, so it leaves no usage row. Your own record and the log line are the trace for those.
A note on /v2/approvals
HiveGate also has an approvals inbox at /v2/approvals, used by the supervisor platform. GET /v2/approvals lists pending requests, and POST /v2/approvals/{job_id}/approve, /deny or /decide records a decision. It can push requests out through notification plugins (webhook, Slack, Telegram, Discord and WhatsApp are built in; GET /v2/approvals/plugins/list shows them). Those requests are also held in process memory. For the agent chat flow in this post, the commit call is what resumes the run.
Try it
- Deploy HiveGate from the deploy page. Create an agent tagged available_tools:email, store a test Gmail token for one user (see the OAuth post), and ask the agent to email you. The run will pause.
- The commit endpoint is in api/routes/v2/agents.py and the Agno resume logic is in agents/hitl.py in the GitHub repo.
Agno's clear_memory tool wipes every user's memories: how to scope Agno agent memory per user
---
title: "Agno's clear_memory tool wipes every user's memories: how to scope Agno agent memory per user"
published: true
canonical_url: memorysync.hashnode.dev
description: "Agno's clear_memory tool deletes all users' memories (issue #9983). Check your flags, wipe one user safely and add a guard."
tags: ai, python, agents, tutorial
---
Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official Agno and MemorySync docs. Code status: on Oct 26, 2026 I ran the same calls as Steps 1, 3 and 4 (the reproduction, the per-user delete and the guard) in scratch scripts on agno 3.1.10 (Python 3.13, a local SQLite file), with no model and no API keys. In Step 5 I only constructed the agent and called clear_memories() with a placeholder key, which raised before any request. No LLM was called and no MemorySync request was made, so memory extraction and recall were not run. The rest is adapted from the docs.
If you run one Agno agent for many users, you probably keep every user's memories in one database and separate them with user_id. agno-agi/agno#9983 shows where that breaks: the clear_memory tool that MemoryManager can hand to the model calls db.clear_memories() with no user filter. One user's session can delete every user's memories.
Below: how to check whether your setup can reach that tool, the settings that keep it away from the model, a per-user wipe that stays scoped, and a small guard you can add today. Agno behaviour comes from the Agno memory docs, the MemoryManager reference and the installed package. I'm building MemorySync, a hosted memory layer, and it gets one short section near the end (if you want it with Agno, see the MemorySync Agno guide).
What the issue found
MemoryManager builds a set of memory tools for model-driven memory work: add_memory, update_memory, delete_memory and clear_memory. The first three pass the current user_id. clear_memory doesn't:
def clear_memory() -> str:
db.clear_memories()
...
The reporter says clear_memories() deletes the whole memories table in every database driver (SQLite, PostgreSQL, MySQL and MongoDB). A fix that routes the tool through the scoped clear_user_memories(user_id=...) is up as PR #9986. On Oct 26, 2026 the issue and the PR were both still open, and agno 3.1.10, the latest release on PyPI that day, still has the unscoped tool.
Step 1: Reproduce it in a few lines (no model needed)
This follows the issue's reproduction. _get_db_tools is a private method; it's used here only to call the tool exactly as the agent would:
from agno.db.sqlite import SqliteDb
from agno.memory import MemoryManager, UserMemory
db = SqliteDb(db_file="memories.db")
db.upsert_user_memory(UserMemory(user_id="user-alice", memory="Alice books window seats."))
db.upsert_user_memory(UserMemory(user_id="user-bob", memory="Bob prefers aisle seats."))
manager = MemoryManager(db=db)
tools = {f.__name__: f for f in manager._get_db_tools(
"user-alice", db, "fixture",
enable_add_memory=True, enable_update_memory=True,
enable_delete_memory=True, enable_clear_memory=True,
)}
tools["clear_memory"]() # a tool call inside Alice's session
print([m.memory for m in db.get_user_memories(user_id="user-bob")])
On agno 3.1.10 this printed Memory cleared successfully and then []. Bob's memory was gone after a call made in Alice's session.
Step 2: Check whether the model can reach clear_memory
In agno 3.1.10, the model only gets this tool in one situation:
- Automatic memory (update_memory_on_run=True): the background memory call is built with enable_clear_memory=False, so clear_memory is never offered. Agno's production best practices recommend automatic memory by default anyway.
- Agentic memory (enable_agentic_memory=True): the agent gets an update_user_memory tool, and the nested memory call offers the tools your MemoryManager flags allow. clear_memory is included only if you set MemoryManager(clear_memories=True).
The MemoryManager reference lists clear_memories as "Allow the manager to clear all memories", default False. So the setting to look for is clear_memories=True. If you need users to be able to say "forget everything about me", use delete_memories=True instead. The agentic memory docs say it lets the tool delete memories for the current user_id:
from agno.agent import Agent
from agno.memory import MemoryManager
agent = Agent(
model=..., # your model
db=db,
memory_manager=MemoryManager(delete_memories=True), # leave clear_memories at False
enable_agentic_memory=True,
)
With these flags, I checked the tool list the memory call would get: add_memory, delete_memory, update_memory, and no clear_memory.
Also look through your own code for MemoryManager.clear() and db.clear_memories(). The reference says clear() "has no user scope and clears the entire memory store".
Step 3: Wipe one user's memories in your own code
For an admin action or a "delete my data" endpoint, call the scoped method with the authenticated user's ID:
manager.clear_user_memories(user_id="user-alice")
# async database: await manager.aclear_user_memories(user_id="user-alice")
In my run, Alice had no memories left afterwards and Bob still had his. Always pass user_id explicitly. Per the reference, user-scoped methods fall back to a user called "default" when it's omitted, and the best practices page says not to rely on it.
Step 4: Add a guard until the fix ships
If you want the global wipe gone no matter which flags someone sets later, subclass your database and make clear_memories() refuse. This is my addition, not something from the Agno docs:
from agno.db.sqlite import SqliteDb
class NoGlobalClearSqliteDb(SqliteDb):
def clear_memories(self) -> None:
raise RuntimeError(
"Global memory wipe is disabled. Use clear_user_memories(user_id=...)."
)
db = NoGlobalClearSqliteDb(db_file="memories.db")
With this in place, running Step 1 again made the clear_memory tool raise instead of deleting, and Bob's memory stayed. clear_user_memories(user_id=...) still worked, because it deletes one user's memories by ID and never calls clear_memories(). The same override works for the PostgreSQL, MySQL or MongoDB classes. I only ran it on SQLite.
Step 5 (optional): Keep memories in MemorySync
MemorySync's Agno guide plugs a MemorySyncDb into Agno's own MemoryManager. Sessions stay in your database, and memories go to MemorySync. Its clear_memories() always raises, and the per-user wipe is an explicit forget_user_memories(user_id):
pip install agno-memorysync
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.memory import MemoryManager
from agno_memorysync import MemorySyncDb
memory_db = MemorySyncDb() # reads MEMORYSYNC_API_KEY
agent = Agent(
db=SqliteDb(db_file="agent.db"), # sessions: local
memory_manager=MemoryManager(db=memory_db), # memories: MemorySync
update_memory_on_run=True,
user_id="customer-42",
)
memory_db.forget_user_memories("customer-42") # one user's memories, returns the count
The guide requires Python 3.10+ and agno 2.8 or newer, and you create the API key in the dashboard under Settings → API Keys. MemorySyncDb only handles memory. If you point sessions or knowledge at it, it raises MemorySyncMemoryOnlyError, so keep those on your own database. In my run, constructing the agent worked, and clear_memories() raised MemorySyncClearAllError before sending anything. You don't need MemorySync for Steps 1 to 4.
Troubleshooting
- All users' memories disappeared after a chat. Check for MemoryManager(clear_memories=True) with agentic memory, and for clear() or clear_memories() calls in your code. Then add the Step 4 guard.
- Memories from different users get mixed. Pass the authenticated user_id on every run and every MemoryManager call. An omitted ID means "default".
- clear_user_memories() raises with an async database. Use aclear_user_memories(). The reference says the sync version requires a synchronous database.
- MemorySyncMemoryOnlyError. You pointed a session or knowledge surface at MemorySyncDb. Only memory_manager= should use it.
Common questions
Does Agno's clear_memory tool delete only the current user's memories?
Not in agno 3.1.10. The tool calls db.clear_memories(), which isn't filtered by user (agno#9983). A fix that scopes it to the calling user is proposed in PR #9986, which was still open on Oct 26, 2026.
How do I delete all memories for one user in Agno?
Call MemoryManager.clear_user_memories(user_id=...), or aclear_user_memories(...) with an async database. The reference describes both as clearing only the selected user's memories (MemoryManager reference).
Is Agno memory isolated per user by default?
Memories are stored and read by user_id. The multi-user chat example scopes history by session_id and memory by user_id. If you omit the ID, Agno uses "default", and clear() / clear_memories() ignore user scope entirely.
Can I use MemorySync as Agno's memory backend?
Yes. MemorySyncDb from agno-memorysync goes into MemoryManager(db=...), while sessions stay in your own database. Its clear_memories() always raises, and per-user wipes use forget_user_memories(user_id) (MemorySync Agno guide).
Sources
- GitHub issue agno-agi/agno#9983 and PR #9986 (both open on 2026-10-26)
- Agno docs: docs.agno.com/memory/overview, /reference/memory/memory, /memory/agent/agentic-memory, /memory/agent/multi-user-multi-session-chat, /memory/best-practices
- agno 3.1.10 source (agno/memory/manager.py: _get_db_tools, create_user_memories, update_memory_task) and PyPI (agno 3.1.10, agno-memorysync 1.1.0)
- MemorySync Agno guide: docs.memorysync.io/guides/agno
Try it
If you want to see a memory survive a full page reload before wiring up an agent, the MemorySync playground runs the save, reload and recall loop in your browser: memorysync.io.
CVE-2026-35002: CVE-2026-35002: Eval Injection and Arbitrary Code Execution in Agno Agent Platform
CVE-2026-35002: Eval Injection and Arbitrary Code Execution in Agno Agent Platform
> Vulnerability ID: CVE-2026-35002
> CVSS Score: 9.8
> Published: 2026-10-27
CVE-2026-35002 is an arbitrary code execution vulnerability in the Agno agent platform (formerly Phidata) affecting all versions prior to 2.3.24. The flaw resides in the model execution logic (libs/agno/agno/models/base.py) and tool serialization logic (libs/agno/agno/tools/function.py), where the application directly passes the field_type string parameter into Python's built-in eval() function to resolve string type names into Python data types. An attacker who can influence FunctionCall arguments via prompt injection or supply untrusted dictionary structures for deserialization can execute arbitrary Python expressions within the context of the host process.
TL;DR
Agno versions prior to 2.3.24 use Python's eval() function to resolve string type names in the field_type parameter during function calling and tool deserialization. Replacing type strings with arbitrary Python expressions allows unauthenticated remote code execution.
---
⚠️ Exploit Status: POC
Technical Details
- CWE ID: CWE-95 (Eval Injection)
- CVSS v3.1 Score: 9.8 (Critical)
- CVSS v4.0 Score: 9.3 (Critical)
- Attack Vector: Network (AV:N)
- Impact: Arbitrary Code Execution (RCE)
- EPSS Score: 1.15% (66.68th Percentile)
- Exploit Status: Proof of Concept (PoC)
- CISA KEV Status: Not Listed
Affected Systems
- Agno Agent Platform (PyPI package: agno)
- Agno: < 2.3.24 (Fixed in: 2.3.24)
Code Analysis
Commit: cbf6755
fix: replace eval() with type mapping [SDK-417] (#5912)
diff --git a/libs/agno/agno/models/base.py b/libs/agno/agno/models/base.py
index abcdef1..cbf6755 100644
--- a/libs/agno/agno/models/base.py
+++ b/libs/agno/agno/models/base.py
@@ -2030,10 +2030,20 @@ def run_function_calls(
user_input_schema = []
for input_field in fc.arguments.get("user_input_fields", []):
field_type = input_field.get("field_type")
- try:
- python_type = eval(field_type) if isinstance(field_type, str) else field_type
- except (NameError, SyntaxError):
- python_type = str # Default to str if type is invalid
+ if isinstance(field_type, str):
+ type_mapping = {
+ "str": str,
+ "int": int,
+ "float": float,
+ "bool": bool,
+ "list": list,
+ "dict": dict,
+ }
+ python_type = type_mapping.get(field_type, str)
+ elif isinstance(field_type, type):
+ python_type = field_type
+ else:
+ python_type = str
user_input_schema.append(
UserInputField(
name=input_field.get("field_name"),
@@ -2185,10 +2195,20 @@ async def arun_function_calls(
user_input_schema = []
for input_field in fc.arguments.get("user_input_fields", []):
field_type = input_field.get("field_type")
- try:
- python_type = eval(field_type) if isinstance(field_type, str) else field_type
- except (NameError, SyntaxError):
- python_type = str # Default to str if type is invalid
+ if isinstance(field_type, str):
+ type_mapping = {
+ "str": str,
+ "int": int,
+ "float": float,
+ "bool": bool,
+ "list": list,
+ "dict": dict,
+ }
+ python_type = type_mapping.get(field_type, str)
+ elif isinstance(field_type, type):
+ python_type = field_type
+ else:
+ python_type = str
user_input_schema.append(
UserInputField(
name=input_field.get("field_name"),
diff --git a/libs/agno/agno/tools/function.py b/libs/agno/agno/tools/function.py
index 1234567..89abcdef 100644
--- a/libs/agno/agno/tools/function.py
+++ b/libs/agno/agno/tools/function.py
@@ -54,9 +54,17 @@ def to_dict(self) -> Dict[str, Any]:
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "UserInputField":
+ type_mapping = {"str": str, "int": int, "float": float, "bool": bool, "list": list, "dict": dict}
+ field_type_raw = data["field_type"]
+ if isinstance(field_type_raw, str):
+ field_type = type_mapping.get(field_type_raw, str)
+ elif isinstance(field_type_raw, type):
+ field_type = field_type_raw
+ else:
+ field_type = str
return cls(
name=data["name"],
- field_type=eval(data["field_type"]), # Convert string type name to actual type
+ field_type=field_type,
description=data["description"],
value=data["value"],
)
Mitigation Strategies
- Upgrade the Agno package to version 2.3.24 or later to eliminate the eval() injection sink.
- Implement strict schema validation on all external dictionary data prior to calling deserialization helpers such as UserInputField.from_dict().
- Restrict network egress and apply least-privilege execution contexts (such as isolated containers or sandboxes) for LLM agent worker processes.
- Use static application security testing (SAST) rules to detect and block dynamic evaluation functions like eval() and exec() in Python codebases.
Remediation Steps:
1. Audit all Python environments and dependency lockfiles using 'pip list | grep -i agno' to locate installations running versions prior to 2.3.24.
2. Update the dependency specification in requirements.txt, pyproject.toml, or Pipfile to require 'agno>=2.3.24'.
3. Execute 'pip install --upgrade "agno>=2.3.24"' and rebuild affected application containers.
4. Rotate any API keys, database credentials, or cloud IAM tokens accessible to the Agno process if the application was exposed to untrusted external inputs prior to patching.
References
- [GitHub Fix Commit: replace eval() with type mapping [SDK-417] (#5912)](github.com/agno-agi/agno/commit/cbf675521d4d22819…)
- Agno v2.3.24 Release Notes
- VulnCheck Advisory: Agno field_type Eval Injection Arbitrary Code Execution
- National Vulnerability Database (NVD) - CVE-2026-35002
- CVE.org Record: CVE-2026-35002
- GitHub Security Advisory: GHSA-77rh-m34w-rv36
- Agno GitHub Repository
- CVE List V5 JSON Record for CVE-2026-35002
---
Read the full report for CVE-2026-35002 on our website for more details including interactive diagrams and full exploit analysis.
Fix "ModuleNotFoundError: No module named 'agno.memory.v2'" (Agno v1 v2+ memory migration)
---
title: "Fix \"ModuleNotFoundError: No module named 'agno.memory.v2'\" (Agno v1 → v2+ memory migration)"
published: true
canonical_url: memorysync.hashnode.dev
description: "After upgrading Agno, imports like agno.memory.v2.memory and agno.storage.sqlite stop resolving. Here's the exact mapping to the current API, verified on Agno 3.1.10, and how to keep user memories working."
tags: ai, python, agents, debugging
---
Disclosure: I'm Mohammed, founder of MemorySync. I drafted this with AI assistance and checked it against Agno's official migration guide and a real install. Code status: on Oct 25, 2026 I installed agno 3.1.10 (Python 3.13) and reproduced the errors below. I then ran the "after" imports, the Agent construction and get_user_memories successfully. No model was called (no key), so memory creation itself was not run.
The error
You upgrade Agno and memory code that used to work fails on import:
ModuleNotFoundError: No module named 'agno.memory.v2'
or, in VS Code / Pylance:
Import "agno.memory.v2.memory" could not be resolved
often with a companion:
ModuleNotFoundError: No module named 'agno.storage'
This was reported in agno-agi/agno#5017 (Agno 2.1.4, Python 3.11). The maintainers' answer was to follow the memory section of the v2 migration guide. I reproduced all three errors on Agno 3.1.10:
$ python -c "from agno.memory.v2.memory import Memory"
ModuleNotFoundError: No module named 'agno.memory.v2'
$ python -c "from agno.storage.sqlite import SqliteStorage"
ModuleNotFoundError: No module named 'agno.storage'
Why it happens
Agno v2 removed the v1 memory and storage modules rather than aliasing them. Per the v2 migration guide:
- Storage classes moved from agno/storage to agno/db and are now called Db classes. One instance serves agents, teams and workflows, and the mode and table_name parameters are gone.
- The MemoryDb classes are deprecated in favour of the main Db classes.
- The Memory class is deprecated. You set update_memory_on_run=True on an Agent that has a db.
The fix: before and after
v1 (breaks):
from agno.agent import Agent
from agno.memory.v2.db.sqlite import SqliteMemoryDb
from agno.memory.v2.memory import Memory
from agno.storage.sqlite import SqliteStorage
memory = Memory(db=SqliteMemoryDb(table_name="user_memories", db_file="agno.db"))
storage = SqliteStorage(table_name="agent_sessions", db_file="agno.db")
agent = Agent(memory=memory, storage=storage, enable_user_memories=True)
v2+ (ran on 3.1.10):
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
db = SqliteDb(db_file="agno.db") # sessions AND memories
agent = Agent(db=db, update_memory_on_run=True)
print(agent.get_user_memories(user_id="john@example.com")) # [] until a run stores something
Things to check after the swap:
1. Pass user_id on every run. Memories are stored per user, so a missing or changing user_id looks like "memory isn't stored".
2. Table names. If you need your old table names, SqliteDb takes session_table= and memory_table= instead of table_name=.
3. Custom memory model or instructions. Build a MemoryManager (from agno.memory import MemoryManager) and pass memory_manager= to the Agent. The parameter names I found on the current Agent are memory_manager, enable_agentic_memory, update_memory_on_run and add_memories_to_context.
4. Existing v1 data. The migration guide links a v1-to-v2 migration script. It says to use the script from the v2 release matching your environment and not to run it under Agno v3, so migrate to v2 first, verify, then upgrade. Back up your database before running anything against it.
5. TypeError: UserMemory.__init__() got an unexpected keyword argument 'summaries'. This is a different, older error from mixed v1 memory data (#2955, Agno 1.4.1). The reporter confirmed it was gone after the maintainers' follow-up, so upgrade first.
If you get ImportError: The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed when importing agno.db.sqlite, run pip install greenlet. I hit this on a clean Python 3.13 venv.
If you want memories outside the local database
update_memory_on_run writes to whatever Db you pass. If you want sessions to stay local but user memories to live in a hosted store shared across instances, MemorySync's agno-memorysync package backs Agno's MemoryManager (guide). The guide says it needs agno 2.8+, and this constructed fine on 3.1.10:
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.memory import MemoryManager
from agno_memorysync import MemorySyncDb # pip install agno-memorysync; needs MEMORYSYNC_API_KEY
agent = Agent(
db=SqliteDb(db_file="agent.db"), # sessions: local
memory_manager=MemoryManager(db=MemorySyncDb()), # memories: hosted
update_memory_on_run=True,
user_id="customer-42",
)
The guide notes that MemorySyncDb is memory-only on purpose. Pointing sessions at it raises MemorySyncMemoryOnlyError, so keep db= local.
Common questions
Is there a compatibility shim for agno.memory.v2? Not in 3.1.10. The import fails. Update the imports.
Do I still need a separate storage object? No. One Db instance handles sessions, memories, metrics, evals and knowledge in separate tables.
My memories are stored but never show up in replies. If you pass a verbatim system_message=, it replaces Agno's built system prompt, which is where memories are injected. Use instructions= instead. (This tip is from MemorySync's Agno guide and applies to any memory db.)
Which Agno version is current? PyPI served 3.1.10 on Oct 25, 2026. The v2 migration steps above still applied to it.
To see a stored fact survive a reload before wiring anything in: try MemorySync in your browser. No signup is needed.
Sources
- agno-agi/agno#5017 (the import error): agno#5017
- agno-agi/agno#2955 (summaries TypeError): agno#2955
- Agno v2 migration guide: docs.agno.com
- Agno, Agent with Memory: docs.agno.com
- MemorySync Agno guide: docs.memorysync.io
The hard part is not creating an agent. It is operating one reliably.
agno (~43k★): Build and manage AI agents in Python. agno-agi/agno
What would you automate with a reliable agent?
9 views
Reduce API surface for BLAKE2 to match OpenSSL semantics
This is an updated version of the original post which reduces the scope of the work on BLAKE2.
I originally wanted to remove non-standard parameters to BLAKE2 to make the module more maintainable in the future as well as SIMD support, but I am planning to only remove SIMD support for now.
I still think it's wrong to use tree-hashing with BLAKE2 since BLAKE3 exists and should be used instead. Nonetheless, after looking at the deprecation PRs (gh-160374 and gh-160375) and the long period this would require to actually remove the code, I don't think it's really worth it. It would at least reduce the impact to downstream users almost to zero. Instead, I suggest that we soft-deprecate the interface for tree hashing in the docs and suggest BLAKE3 instead.
----
Original post:
I want to reduce BLAKE2 in hashlib to a minimal API matching OpenSSL. More precisely, BLAKE2 is kept without SIMD and matches the API of OpenSSL's BLAKE2 (digest, MAC): data and digest_size for the hash, and salt/person with a key. Tree hashing, salt/person without a key, and HMAC-BLAKE2 will be deprecated in 3.16 and removed in 3.21. If necessary, the deprecation period will be extended to a few more releases but I really want to get rid of the annoying API surface.
BLAKE2 is already a keyed hash function, it doesn't make sense to use HMAC on top of it, despite some libraries using the HMAC-BLAKE2 HACL* sources for that (see gh-138114).
In an ideal world, I would remove BLAKE2 and recommend BLAKE3 or SHA-based alternatives. Note that currently (and likely in the future), there are no plans to add BLAKE3 to hashlib (see gh-83479). For a fast secure hash, BLAKE3 is available as the blake3 package. It is maintained by the BLAKE3 authors, has a hashlib-like API, supports keyed hashing and key derivation, and has wheels for all major platforms.
Maintenance pain
BLAKE2 has been in hashlib since 3.6, first as a custom module with our own implementation, then as a wrapper around the vendored HACL* sources (support for HACL* HMAC was added in 3.14, together with support for BLAKE2 despite numerous shortcomings on some platforms).
BLAKE2 has the largest API in hashlib (key, salt, person and seven tree-hashing parameters). Most of its maintenance cost comes from the SIMD code: it needs per-platform compiler flags and CPU detection as well as dedicated code paths for the vectorized versions, is only enabled on (some) x86-64, and usually breaks on non-Tier-1 platforms:
- gh-123748: macOS universal2 builds failed (SIMD256 compiles for x86-64 but not ARM64);
- gh-124304: SIMD had to be disabled on older Android versions;
- gh-126458: SIMD support wrongly detected under WASI;
- gh-128134: missing aligned_alloc check on older macOS;
- gh-130213: older toolchains failed without the SIMD flags;
- gh-133042: duplicate symbols when cross-compiling for Emscripten (still unfixed);
- gh-138114: HMAC-BLAKE2 does not properly use the SIMD implementations (still unfixed).
The module also needed several correctness fixes (gh-152851, gh-155830, gh-155835).
Impact
GitHub code search (excluding forks, CPython copies and vendored site-packages) finds about 34,000 Python files using hashlib.blake2b, 4,800 using hashlib.blake2s, and 300 using hashlib.new("blake2b") or hashlib.new("blake2s").
62 of the 500 most-starred active Python repositories on GitHub use BLAKE2, e.g. PyTorch, Transformers, Home Assistant, Sentry, Streamlit, Gradio and Airflow.
Of 1,753 sampled call sites (1,420 repositories), 1,649 (1,336 repositories) only use digest_size, 59 (51 repositories) use key, and 45 (38 repositories) use salt or person without a key. None use tree hashing. Most of them only need a fast hash, for which SHA-2, SHA-3 or BLAKE3 also work. This includes:
- projects that store digests, such as Home Assistant (cache filenames), Mathesar (database identifiers) and Onyx (search-index IDs);
- projects that depend on the exact BLAKE2 output, such as networkx (graph hashes), LangChain (indexing), apprise and a minisign verifier (BLAKE2b-512 signatures), copyparty (client checksums);
- projects that use keyed BLAKE2 such as agno (MinHash) and openai-python (Bedrock helper); or
- projects such as PyPI or Poetry for checksums, Warehouse for storage and publishing (though it looks like a legacy API), or python-tuf.
One popular project affected by this change would be ComfyUI (which uses person without a key). In particular, I don't expect too much of downstream breakage (considering the long deprecation period) but I would appreciate if anyone against this proposal could explain why SIMD BLAKE2 should be kept (note that I already plan to remove SIMD support in 3.16 without a deprecation notice as it created too many issues, while exposing the OpenSSL implementation).
Note that projects can rely on nacl.hash.blake2b (PyNaCl does not offer a 2s version) or PyCryptodome for blake2b and blake2s. AFAIK, there is no PyPI packages providing the tree-hashing feature but the latter is neither standardized by an RFC nor used in the wild.
----
> AI disclosure: Opus 5.5 was used for collecting and classifying data.
4 likes · 1 comment
Deploying Agno agents to production as a REST API
Originally published on hivegate.dev.
An Agno agent that works in a script is a few lines of Python. Turning it into something a web or mobile app can call for thousands of users takes more: an HTTP endpoint, a conversation per user, history that survives restarts, API keys, model keys kept on the server, and a container you can deploy.
This post shows how HiveGate, an MIT-licensed FastAPI service built on Agno, handles each of those. It also covers when you should skip HiveGate and use Agno's own AgentOS.
First: you may not need HiveGate
Agno ships AgentOS, which serves agents defined in code over HTTP, with sessions, human approval, RBAC and tracing:
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.google import Gemini
from agno.os import AgentOS
support_agent = Agent(
name="support-agent",
model=Gemini(id="gemini-3-flash-preview"),
db=PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5432/ai"),
add_history_to_context=True,
)
app = AgentOS(agents=[support_agent]).get_app()
If your agents live in your codebase and that covers what you need, use AgentOS. It's maintained by the Agno team and tracks Agno releases.
HiveGate is for a different case: agents that act for your users in their own Google or Microsoft accounts, and agents, prompts and skills that you manage as data through an API instead of shipping code for each change.
Agents are records, not code
In HiveGate you create an agent with a POST. The template becomes the agent's system prompt, stored as a versioned prompt in Postgres. The config block maps onto Agno's Agent options.
curl -X POST http://localhost:8000/v2/agents \
-H "X-API-Key: $HIVEGATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "support-agent",
"name": "support-agent",
"template": "You are the support assistant for Acme. Answer briefly and cite the help center when you can.",
"description": "Customer support for Acme",
"tags": ["available_tools:email,calendar"],
"config": {
"enable_memory": true,
"enable_history": true,
"num_history_runs": 3
}
}'
The available_tools: tag switches on HiveGate's built-in toolkits for this agent. Leave it out and the agent gets none of them. Agents that need other tools can point at MCP servers over Streamable HTTP through config.worker_config.mcp_servers.
Changing the prompt later is a PUT /v2/prompts/support_agent (the prompt ID is the agent's name in snake case). The next chat picks up the new template, so a prompt fix doesn't need a redeploy.
Chatting, with sessions per user
Every chat request names a user_id and a session_id. Both are required, as are timezone and locale:
curl -X POST http://localhost:8000/v2/agents/support-agent/chat \
-H "X-API-Key: $HIVEGATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "My invoice from last month looks wrong.",
"user_id": "user-1842",
"session_id": "ticket-5521",
"timezone": "America/New_York",
"locale": "en-US",
"stream": false
}'
The non-streaming response is a JSON object with content, session_id, run_id, status, the tools the agent called, and token_usage for that call.
Here is what happens to those two IDs:
- History. Agno stores each run in a Postgres session table, one table per agent. With enable_history on, the last num_history_runs runs of the same session go back into the model's context. A follow-up with the same session_id continues the conversation, even after a restart or on another replica.
- Memory. With enable_memory on, Agno's memory manager keeps facts about the user across sessions, keyed by user_id.
- Agent cache. HiveGate builds the Agno Agent once per (prompt, agent, model, user, session) and caches it in the process, so later turns skip setup.
Per-request context goes into Agno's session_state. HiveGate fills it from the request: the user's name and profile (from user_profile), timezone and locale. If you send a tenant_profile, knowledge search is filtered to that tenant's documents, so one deployment can serve several customers without mixing their data.
Streaming
stream defaults to true. The response is Server-Sent Events, one event: message per Agno event, each with a JSON data line:
import json
import httpx
payload = {
"message": "Summarize my open tickets.",
"user_id": "user-1842",
"session_id": "ticket-5521",
"timezone": "UTC",
"locale": "en",
}
with httpx.stream(
"POST",
"http://localhost:8000/v2/agents/support-agent/chat",
headers={"X-API-Key": HIVEGATE_KEY},
json=payload,
timeout=120,
) as response:
for line in response.iter_lines():
if not line.startswith("data: "):
continue
event = json.loads(line[len("data: "):])
if event.get("event") == "RunContent" and isinstance(event.get("content"), str):
print(event["content"], end="", flush=True)
If a model call fails mid-stream, you get an event: error with a generic message. The details stay in the server log, so provider internals don't leak to clients.
Picking the model per request
A request can set model. Otherwise HiveGate uses DEFAULT_CHAT_MODEL, and falls back to gemini-3-flash-preview if that's unset. Besides pinned IDs such as claude-sonnet-4-6, it accepts aliases like anthropic:sonnet-latest or google:flash-latest. An alias resolves to the vendor's newest model in that tier and never moves you up to a pricier tier. Model keys stay in the server's environment; clients never see them.
API keys and cost per user
/v2/* requires an X-API-Key. Create keys with the admin secret:
curl -X POST http://localhost:8000/admin/api-keys \
-H "X-Admin-Secret: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"name": "acme-web", "rate_limit": 1000}'
The raw key is in the api_key field of the response, and it's shown only once. For local work, AUTH_DISABLED=true skips the check. Never set it on a deployment other people can reach.
Every model call is metered: tokens, cache reads, tool calls, duration and USD cost go into a token_usage table, including failed and abandoned runs. To see which users cost the most:
curl -H "X-API-Key: $USAGE_KEY" \
"http://localhost:8000/usage/summary?window=7d&group_by=user"
The usage endpoints accept a key with the usage:read scope. A key whose only scope is usage:read is refused on /v2, so a dashboard key can't call agents.
Running it
Locally, Docker Compose starts Postgres and Qdrant and seeds demo agents:
git clone https://github.com/hivegate-ai/hivegate && cd hivegate
cp .env.example .env # set GOOGLE_API_KEY, uncomment AUTH_DISABLED=true
docker compose up -d
./scripts/dev_setup.sh && source .venv/bin/activate && ./scripts/start_server.sh
For a server, use the prebuilt multi-arch image ghcr.io/hivegate-ai/hivegate:latest. The deploy page has one-click buttons for Render, Railway and Koyeb, plus Kubernetes and AWS, Azure and GCP manifests. The settings that matter:
| Variable | Why |
|---|---|
| DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_DATABASE | Postgres for agents, sessions, tokens and usage |
| ADMIN_SECRET | Protects /admin/*, where API keys are created |
| GOOGLE_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY | At least one model provider |
| SECRET_TOKEN_ENC_KEY | Fernet key that encrypts stored user OAuth tokens |
| QDRANT_URL | Optional, for per-tenant knowledge |
GET /health is the health check. The deploy page ends with a five-step smoke test: health, list agents, create an agent, one real LLM call, then cleanup.
What to know before scaling out
Conversation history is in Postgres, so any replica can serve the next turn. Two things live in process memory: the cached Agent objects, and runs that are paused waiting for a human to approve a tool call. A paused run has to be resumed on the process that paused it. If you use approvals, run one replica or route each session to the same instance. The post on approvals covers that flow.
When to use which
- Agents defined in code, one team, standard Agno features: AgentOS.
- Agents that act in each user's Gmail, Outlook or calendar; agents and prompts edited through an API; per-tenant knowledge; supervisor teams with container workers: HiveGate.
Try it
- Deploy in one click from the deploy page.
- Read the source, open an issue or star the repo on GitHub.
If this is useful, a star helps other people find the project: ⭐ github.com/hivegate-ai/hivegate.
1 reaction
想让 AI Agent 调工具,还能查看运行记录?🤖
Agno 提供 Python Agent 框架和 AgentOS,方便连接工具、部署及追踪运行。🧩
agno-agi/agno
#AIAgent #Python #浇朋友 #蓝朋友 #互关 #fo
48 views
Oct 6: Agno (still open)
A queue with zero doors doesn't need a longer timeout. It needs a door.
I opened a fix to reject zero/negative concurrency in two page fetchers before requests can wait forever.
Local checks; PR open.
agno#10847
Oct 6 rewind (2026): Agno
A failed save shouldn't take the last good save down with it.
I staged regular JSON tables before replacing them. Direct-write fallbacks still remain.
Tiny files, big trust issues.
Submitted Oct 3; merged Oct 6.
agno#10794
3 views
Mentions
Human approval before risky agent tool calls Originally published on hivegate.dev. Letting an agent search an inbox is low risk. Letting it send an email, cancel a meeting or permanently delete a message is not. A wrong recipient or a hallucinated date becomes a real-world mistake. The usual answer is human-in-the-loop: the agent proposes the call, a person reviews it, and only then does it run. This post shows how that works in HiveGate, an open-source FastAPI service for Agno agents. The run pauses, your app gets the proposed call, and a second request approves, edits or denies it. Which tools pause Agno lets a toolkit mark tools as requires_confirmation. When the model calls one, Agno stops the run before executing it. HiveGate's built-in toolkits mark every write: - Email: send_email, create_draft, send_draft, trash_email, delete_email_permanently, modify_labels - Calendar: schedule_meeting, schedule_meeting_find_time, cancel_meeting - Contacts: create_contact, update_contact, delete_contact - Drive: read_file - Workers: run_claude_code, run_managed_agent Read-only tools such as search_emails and list_events run without a pause. Your own Agno toolkits opt in the same way, through requires_confirmation_tools=[...]. Tools from MCP servers don't have an approval gate yet. That's on the roadmap. Step 1: the run pauses A normal chat request: curl -X POST http://localhost:8000/v2/agents/support-agent/chat \ -H "X-API-Key: $HIVEGATE_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Email jordan@acme.example that the renewal call moves to Thursday 3pm.", "user_id": "user-1842", "session_id": "inbox-1", "timezone": "America/Chicago", "locale": "en-US", "stream": false, "user_profile": { "profile_id": "user-1842", "email": "dana@acme.example", "full_name": "Dana Lee", "role": "Account manager", "tenant_id": "acme" } }' Instead of a finished answer, the response has status: "PAUSED", a run_id, and the pending call with its arguments (trimmed here): { "agent_id": "support-agent", "session_id": "inbox-1", "status": "PAUSED", "run_id": "6f1c9a2e-...", "tools": [ { "tool_call_id": "call_8Jx2...", "tool_name": "send_email", "requires_confirmation": true, "tool_args": { "to": ["jordan@acme.example"], "subject": "Renewal call moved to Thursday", "body_text": "Hi Jordan, the renewal call is now Thursday at 3pm CT. Thanks, Dana" }, "result": null } ] } Nothing has been sent yet. With "stream": true, you get the same data as an SSE event whose event field is RunPaused, and then the stream ends. Show tool_name and tool_args to the user, or to whoever reviews actions for them, in whatever form fits your app: a confirmation card, an editable form, a Slack message. Step 2: approve, edit or deny The decision goes to POST /v2/agents/{agent_id}/chat/commit with the run_id, the same user_id and session_id, and one entry per pending tool in updated_tools. Approve as proposed: curl -X POST http://localhost:8000/v2/agents/support-agent/chat/commit \ -H "X-API-Key: $HIVEGATE_KEY" \ -H "Content-Type: application/json" \ -d '{ "run_id": "6f1c9a2e-...", "user_id": "user-1842", "session_id": "inbox-1", "stream": false, "updated_tools": [ {"tool_call_id": "call_8Jx2...", "confirmed": true} ] }' The agent resumes, the email is sent through the user's own Gmail or Outlook account, and the response has the agent's final message, status: "completed" and the token usage for the resumed part. Edit, then approve. The reviewer fixed the time, so send the corrected arguments with the approval. tool_args replaces the proposed arguments, so send the full set: {"tool_call_id": "call_8Jx2...", "confirmed": true, "tool_args": { "to": ["jordan@acme.example"], "subject": "Renewal call moved to Thursday", "body_text": "Hi Jordan, the renewal call is now Thursday at 4pm CT. Thanks, Dana" }, "confirmation_note": "Corrected time to 4pm"} Deny: {"tool_call_id": "call_8Jx2...", "confirmed": false, "confirmation_note": "Wrong recipient"} If every pending tool is denied, HiveGate doesn't resume the run at all. It returns status: "cancelled" with the content "Tool execution cancelled by user.", and no model call is made. If you approve some tools and deny others, the run resumes: Agno executes the approved ones, skips the denied ones, and tells the model they were rejected. The fields HiveGate accepts in each updated_tools entry are confirmed, tool_args (or args) and confirmation_note. Anything else is ignored. The same thing from Python A minimal backend helper, with the review step left to your UI: import httpx BASE = "http://localhost:8000/v2/agents/support-agent" HEADERS = {"X-API-Key": HIVEGATE_KEY} def chat(message: str, user: dict, session_id: str) -> dict: body = { "message": message, "user_id": user["profile_id"], "session_id": session_id, "timezone": "UTC", "locale": "en", "stream": False, "user_profile": user, } return httpx.post(f"{BASE}/chat", headers=HEADERS, json=body, timeout=120).json() def decide(paused: dict, user: dict, decisions: list[dict]) -> dict: """decisions: one {"tool_call_id", "confirmed", optional "tool_args", "confirmation_note"} per pending tool.""" body = { "run_id": paused["run_id"], "user_id": user["profile_id"], "session_id": paused["session_id"], "stream": False, "updated_tools": decisions, } response = httpx.post(f"{BASE}/chat/commit", headers=HEADERS, json=body, timeout=120) if response.status_code == 410: raise RuntimeError("Paused run is gone; ask the agent again.") response.raise_for_status() return response.json() reply = chat("Cancel my 2pm with Jordan.", user, "inbox-1") if reply["status"] == "PAUSED": pending = [t for t in reply["tools"] if t.get("requires_confirmation")] decisions = review_in_ui(pending) # your code: returns approve/edit/deny per tool reply = decide(reply, user, decisions) print(reply["content"]) If the chat request named a model, send the same model on commit. HiveGate uses it to find the paused agent. Where paused runs live A paused run is held in the memory of the HiveGate process that paused it, until it's committed. If the process restarts, or the commit lands on a different replica, commit returns 410 Gone ("Confirmation time window has elapsed"). Two practical consequences: - Run a single replica, or route each session to the same instance, when you use approvals. - Treat a 410 as "the proposal expired": ask the agent again rather than retrying the commit. The conversation itself is stored in Postgres, so the user doesn't lose their history when a pending action expires. The audit trail Here is what HiveGate records about a decision: - The run, in the agent's session. Agno stores each run in the agent's Postgres session table. A resumed run's tool executions carry the arguments that actually ran (including any edits), requires_confirmation, confirmed and the confirmation_note. - The cost of the resumed run. The resumed part is metered like any other call: a row in token_usage with the user, session, API key name, number of tool calls, status and USD cost. You can list them with GET /usage/calls?agent=support-agent&order=recent, which needs a key with the usage:read scope. - Logs. Every commit is logged with its run_id, and a fully denied run is logged as such. Set OTEL_TRACING_BACKEND=otlp and OTEL_LOGGING_BACKEND=otlp (Sentry and Logtail are also supported) to send traces and logs to your observability stack. What it doesn't record is which person clicked approve. HiveGate sees the API key that made the commit, not your end user's identity. If you need that for compliance, record the reviewer and the decision in your own database before you call commit. You can also put the reviewer in confirmation_note, which is stored with the tool execution. A fully denied run is never resumed, so it leaves no usage row. Your own record and the log line are the trace for those. A note on /v2/approvals HiveGate also has an approvals inbox at /v2/approvals, used by the supervisor platform. GET /v2/approvals lists pending requests, and POST /v2/approvals/{job_id}/approve, /deny or /decide records a decision. It can push requests out through notification plugins (webhook, Slack, Telegram, Discord and WhatsApp are built in; GET /v2/approvals/plugins/list shows them). Those requests are also held in process memory. For the agent chat flow in this post, the commit call is what resumes the run. Try it - Deploy HiveGate from the deploy page. Create an agent tagged available_tools:email, store a test Gmail token for one user (see the OAuth post), and ask the agent to email you. The run will pause. - The commit endpoint is in api/routes/v2/agents.py and the Agno resume logic is in agents/hitl.py in the GitHub repo.
Agno's clear_memory tool wipes every user's memories: how to scope Agno agent memory per user --- title: "Agno's clear_memory tool wipes every user's memories: how to scope Agno agent memory per user" published: true canonical_url: memorysync.hashnode.dev description: "Agno's clear_memory tool deletes all users' memories (issue #9983). Check your flags, wipe one user safely and add a guard." tags: ai, python, agents, tutorial --- Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official Agno and MemorySync docs. Code status: on Oct 26, 2026 I ran the same calls as Steps 1, 3 and 4 (the reproduction, the per-user delete and the guard) in scratch scripts on agno 3.1.10 (Python 3.13, a local SQLite file), with no model and no API keys. In Step 5 I only constructed the agent and called clear_memories() with a placeholder key, which raised before any request. No LLM was called and no MemorySync request was made, so memory extraction and recall were not run. The rest is adapted from the docs. If you run one Agno agent for many users, you probably keep every user's memories in one database and separate them with user_id. agno-agi/agno#9983 shows where that breaks: the clear_memory tool that MemoryManager can hand to the model calls db.clear_memories() with no user filter. One user's session can delete every user's memories. Below: how to check whether your setup can reach that tool, the settings that keep it away from the model, a per-user wipe that stays scoped, and a small guard you can add today. Agno behaviour comes from the Agno memory docs, the MemoryManager reference and the installed package. I'm building MemorySync, a hosted memory layer, and it gets one short section near the end (if you want it with Agno, see the MemorySync Agno guide). What the issue found MemoryManager builds a set of memory tools for model-driven memory work: add_memory, update_memory, delete_memory and clear_memory. The first three pass the current user_id. clear_memory doesn't: def clear_memory() -> str: db.clear_memories() ... The reporter says clear_memories() deletes the whole memories table in every database driver (SQLite, PostgreSQL, MySQL and MongoDB). A fix that routes the tool through the scoped clear_user_memories(user_id=...) is up as PR #9986. On Oct 26, 2026 the issue and the PR were both still open, and agno 3.1.10, the latest release on PyPI that day, still has the unscoped tool. Step 1: Reproduce it in a few lines (no model needed) This follows the issue's reproduction. _get_db_tools is a private method; it's used here only to call the tool exactly as the agent would: from agno.db.sqlite import SqliteDb from agno.memory import MemoryManager, UserMemory db = SqliteDb(db_file="memories.db") db.upsert_user_memory(UserMemory(user_id="user-alice", memory="Alice books window seats.")) db.upsert_user_memory(UserMemory(user_id="user-bob", memory="Bob prefers aisle seats.")) manager = MemoryManager(db=db) tools = {f.__name__: f for f in manager._get_db_tools( "user-alice", db, "fixture", enable_add_memory=True, enable_update_memory=True, enable_delete_memory=True, enable_clear_memory=True, )} tools["clear_memory"]() # a tool call inside Alice's session print([m.memory for m in db.get_user_memories(user_id="user-bob")]) On agno 3.1.10 this printed Memory cleared successfully and then []. Bob's memory was gone after a call made in Alice's session. Step 2: Check whether the model can reach clear_memory In agno 3.1.10, the model only gets this tool in one situation: - Automatic memory (update_memory_on_run=True): the background memory call is built with enable_clear_memory=False, so clear_memory is never offered. Agno's production best practices recommend automatic memory by default anyway. - Agentic memory (enable_agentic_memory=True): the agent gets an update_user_memory tool, and the nested memory call offers the tools your MemoryManager flags allow. clear_memory is included only if you set MemoryManager(clear_memories=True). The MemoryManager reference lists clear_memories as "Allow the manager to clear all memories", default False. So the setting to look for is clear_memories=True. If you need users to be able to say "forget everything about me", use delete_memories=True instead. The agentic memory docs say it lets the tool delete memories for the current user_id: from agno.agent import Agent from agno.memory import MemoryManager agent = Agent( model=..., # your model db=db, memory_manager=MemoryManager(delete_memories=True), # leave clear_memories at False enable_agentic_memory=True, ) With these flags, I checked the tool list the memory call would get: add_memory, delete_memory, update_memory, and no clear_memory. Also look through your own code for MemoryManager.clear() and db.clear_memories(). The reference says clear() "has no user scope and clears the entire memory store". Step 3: Wipe one user's memories in your own code For an admin action or a "delete my data" endpoint, call the scoped method with the authenticated user's ID: manager.clear_user_memories(user_id="user-alice") # async database: await manager.aclear_user_memories(user_id="user-alice") In my run, Alice had no memories left afterwards and Bob still had his. Always pass user_id explicitly. Per the reference, user-scoped methods fall back to a user called "default" when it's omitted, and the best practices page says not to rely on it. Step 4: Add a guard until the fix ships If you want the global wipe gone no matter which flags someone sets later, subclass your database and make clear_memories() refuse. This is my addition, not something from the Agno docs: from agno.db.sqlite import SqliteDb class NoGlobalClearSqliteDb(SqliteDb): def clear_memories(self) -> None: raise RuntimeError( "Global memory wipe is disabled. Use clear_user_memories(user_id=...)." ) db = NoGlobalClearSqliteDb(db_file="memories.db") With this in place, running Step 1 again made the clear_memory tool raise instead of deleting, and Bob's memory stayed. clear_user_memories(user_id=...) still worked, because it deletes one user's memories by ID and never calls clear_memories(). The same override works for the PostgreSQL, MySQL or MongoDB classes. I only ran it on SQLite. Step 5 (optional): Keep memories in MemorySync MemorySync's Agno guide plugs a MemorySyncDb into Agno's own MemoryManager. Sessions stay in your database, and memories go to MemorySync. Its clear_memories() always raises, and the per-user wipe is an explicit forget_user_memories(user_id): pip install agno-memorysync from agno.agent import Agent from agno.db.sqlite import SqliteDb from agno.memory import MemoryManager from agno_memorysync import MemorySyncDb memory_db = MemorySyncDb() # reads MEMORYSYNC_API_KEY agent = Agent( db=SqliteDb(db_file="agent.db"), # sessions: local memory_manager=MemoryManager(db=memory_db), # memories: MemorySync update_memory_on_run=True, user_id="customer-42", ) memory_db.forget_user_memories("customer-42") # one user's memories, returns the count The guide requires Python 3.10+ and agno 2.8 or newer, and you create the API key in the dashboard under Settings → API Keys. MemorySyncDb only handles memory. If you point sessions or knowledge at it, it raises MemorySyncMemoryOnlyError, so keep those on your own database. In my run, constructing the agent worked, and clear_memories() raised MemorySyncClearAllError before sending anything. You don't need MemorySync for Steps 1 to 4. Troubleshooting - All users' memories disappeared after a chat. Check for MemoryManager(clear_memories=True) with agentic memory, and for clear() or clear_memories() calls in your code. Then add the Step 4 guard. - Memories from different users get mixed. Pass the authenticated user_id on every run and every MemoryManager call. An omitted ID means "default". - clear_user_memories() raises with an async database. Use aclear_user_memories(). The reference says the sync version requires a synchronous database. - MemorySyncMemoryOnlyError. You pointed a session or knowledge surface at MemorySyncDb. Only memory_manager= should use it. Common questions Does Agno's clear_memory tool delete only the current user's memories? Not in agno 3.1.10. The tool calls db.clear_memories(), which isn't filtered by user (agno#9983). A fix that scopes it to the calling user is proposed in PR #9986, which was still open on Oct 26, 2026. How do I delete all memories for one user in Agno? Call MemoryManager.clear_user_memories(user_id=...), or aclear_user_memories(...) with an async database. The reference describes both as clearing only the selected user's memories (MemoryManager reference). Is Agno memory isolated per user by default? Memories are stored and read by user_id. The multi-user chat example scopes history by session_id and memory by user_id. If you omit the ID, Agno uses "default", and clear() / clear_memories() ignore user scope entirely. Can I use MemorySync as Agno's memory backend? Yes. MemorySyncDb from agno-memorysync goes into MemoryManager(db=...), while sessions stay in your own database. Its clear_memories() always raises, and per-user wipes use forget_user_memories(user_id) (MemorySync Agno guide). Sources - GitHub issue agno-agi/agno#9983 and PR #9986 (both open on 2026-10-26) - Agno docs: docs.agno.com/memory/overview, /reference/memory/memory, /memory/agent/agentic-memory, /memory/agent/multi-user-multi-session-chat, /memory/best-practices - agno 3.1.10 source (agno/memory/manager.py: _get_db_tools, create_user_memories, update_memory_task) and PyPI (agno 3.1.10, agno-memorysync 1.1.0) - MemorySync Agno guide: docs.memorysync.io/guides/agno Try it If you want to see a memory survive a full page reload before wiring up an agent, the MemorySync playground runs the save, reload and recall loop in your browser: memorysync.io.
CVE-2026-35002: CVE-2026-35002: Eval Injection and Arbitrary Code Execution in Agno Agent Platform CVE-2026-35002: Eval Injection and Arbitrary Code Execution in Agno Agent Platform > Vulnerability ID: CVE-2026-35002 > CVSS Score: 9.8 > Published: 2026-10-27 CVE-2026-35002 is an arbitrary code execution vulnerability in the Agno agent platform (formerly Phidata) affecting all versions prior to 2.3.24. The flaw resides in the model execution logic (libs/agno/agno/models/base.py) and tool serialization logic (libs/agno/agno/tools/function.py), where the application directly passes the field_type string parameter into Python's built-in eval() function to resolve string type names into Python data types. An attacker who can influence FunctionCall arguments via prompt injection or supply untrusted dictionary structures for deserialization can execute arbitrary Python expressions within the context of the host process. TL;DR Agno versions prior to 2.3.24 use Python's eval() function to resolve string type names in the field_type parameter during function calling and tool deserialization. Replacing type strings with arbitrary Python expressions allows unauthenticated remote code execution. --- ⚠️ Exploit Status: POC Technical Details - CWE ID: CWE-95 (Eval Injection) - CVSS v3.1 Score: 9.8 (Critical) - CVSS v4.0 Score: 9.3 (Critical) - Attack Vector: Network (AV:N) - Impact: Arbitrary Code Execution (RCE) - EPSS Score: 1.15% (66.68th Percentile) - Exploit Status: Proof of Concept (PoC) - CISA KEV Status: Not Listed Affected Systems - Agno Agent Platform (PyPI package: agno) - Agno: < 2.3.24 (Fixed in: 2.3.24) Code Analysis Commit: cbf6755 fix: replace eval() with type mapping [SDK-417] (#5912) diff --git a/libs/agno/agno/models/base.py b/libs/agno/agno/models/base.py index abcdef1..cbf6755 100644 --- a/libs/agno/agno/models/base.py +++ b/libs/agno/agno/models/base.py @@ -2030,10 +2030,20 @@ def run_function_calls( user_input_schema = [] for input_field in fc.arguments.get("user_input_fields", []): field_type = input_field.get("field_type") - try: - python_type = eval(field_type) if isinstance(field_type, str) else field_type - except (NameError, SyntaxError): - python_type = str # Default to str if type is invalid + if isinstance(field_type, str): + type_mapping = { + "str": str, + "int": int, + "float": float, + "bool": bool, + "list": list, + "dict": dict, + } + python_type = type_mapping.get(field_type, str) + elif isinstance(field_type, type): + python_type = field_type + else: + python_type = str user_input_schema.append( UserInputField( name=input_field.get("field_name"), @@ -2185,10 +2195,20 @@ async def arun_function_calls( user_input_schema = [] for input_field in fc.arguments.get("user_input_fields", []): field_type = input_field.get("field_type") - try: - python_type = eval(field_type) if isinstance(field_type, str) else field_type - except (NameError, SyntaxError): - python_type = str # Default to str if type is invalid + if isinstance(field_type, str): + type_mapping = { + "str": str, + "int": int, + "float": float, + "bool": bool, + "list": list, + "dict": dict, + } + python_type = type_mapping.get(field_type, str) + elif isinstance(field_type, type): + python_type = field_type + else: + python_type = str user_input_schema.append( UserInputField( name=input_field.get("field_name"), diff --git a/libs/agno/agno/tools/function.py b/libs/agno/agno/tools/function.py index 1234567..89abcdef 100644 --- a/libs/agno/agno/tools/function.py +++ b/libs/agno/agno/tools/function.py @@ -54,9 +54,17 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls, data: Dict[str, Any]) -> "UserInputField": + type_mapping = {"str": str, "int": int, "float": float, "bool": bool, "list": list, "dict": dict} + field_type_raw = data["field_type"] + if isinstance(field_type_raw, str): + field_type = type_mapping.get(field_type_raw, str) + elif isinstance(field_type_raw, type): + field_type = field_type_raw + else: + field_type = str return cls( name=data["name"], - field_type=eval(data["field_type"]), # Convert string type name to actual type + field_type=field_type, description=data["description"], value=data["value"], ) Mitigation Strategies - Upgrade the Agno package to version 2.3.24 or later to eliminate the eval() injection sink. - Implement strict schema validation on all external dictionary data prior to calling deserialization helpers such as UserInputField.from_dict(). - Restrict network egress and apply least-privilege execution contexts (such as isolated containers or sandboxes) for LLM agent worker processes. - Use static application security testing (SAST) rules to detect and block dynamic evaluation functions like eval() and exec() in Python codebases. Remediation Steps: 1. Audit all Python environments and dependency lockfiles using 'pip list | grep -i agno' to locate installations running versions prior to 2.3.24. 2. Update the dependency specification in requirements.txt, pyproject.toml, or Pipfile to require 'agno>=2.3.24'. 3. Execute 'pip install --upgrade "agno>=2.3.24"' and rebuild affected application containers. 4. Rotate any API keys, database credentials, or cloud IAM tokens accessible to the Agno process if the application was exposed to untrusted external inputs prior to patching. References - [GitHub Fix Commit: replace eval() with type mapping [SDK-417] (#5912)](github.com/agno-agi/agno/commit/cbf675521d4d22819…) - Agno v2.3.24 Release Notes - VulnCheck Advisory: Agno field_type Eval Injection Arbitrary Code Execution - National Vulnerability Database (NVD) - CVE-2026-35002 - CVE.org Record: CVE-2026-35002 - GitHub Security Advisory: GHSA-77rh-m34w-rv36 - Agno GitHub Repository - CVE List V5 JSON Record for CVE-2026-35002 --- Read the full report for CVE-2026-35002 on our website for more details including interactive diagrams and full exploit analysis.
Fix "ModuleNotFoundError: No module named 'agno.memory.v2'" (Agno v1 v2+ memory migration) --- title: "Fix \"ModuleNotFoundError: No module named 'agno.memory.v2'\" (Agno v1 → v2+ memory migration)" published: true canonical_url: memorysync.hashnode.dev description: "After upgrading Agno, imports like agno.memory.v2.memory and agno.storage.sqlite stop resolving. Here's the exact mapping to the current API, verified on Agno 3.1.10, and how to keep user memories working." tags: ai, python, agents, debugging --- Disclosure: I'm Mohammed, founder of MemorySync. I drafted this with AI assistance and checked it against Agno's official migration guide and a real install. Code status: on Oct 25, 2026 I installed agno 3.1.10 (Python 3.13) and reproduced the errors below. I then ran the "after" imports, the Agent construction and get_user_memories successfully. No model was called (no key), so memory creation itself was not run. The error You upgrade Agno and memory code that used to work fails on import: ModuleNotFoundError: No module named 'agno.memory.v2' or, in VS Code / Pylance: Import "agno.memory.v2.memory" could not be resolved often with a companion: ModuleNotFoundError: No module named 'agno.storage' This was reported in agno-agi/agno#5017 (Agno 2.1.4, Python 3.11). The maintainers' answer was to follow the memory section of the v2 migration guide. I reproduced all three errors on Agno 3.1.10: $ python -c "from agno.memory.v2.memory import Memory" ModuleNotFoundError: No module named 'agno.memory.v2' $ python -c "from agno.storage.sqlite import SqliteStorage" ModuleNotFoundError: No module named 'agno.storage' Why it happens Agno v2 removed the v1 memory and storage modules rather than aliasing them. Per the v2 migration guide: - Storage classes moved from agno/storage to agno/db and are now called Db classes. One instance serves agents, teams and workflows, and the mode and table_name parameters are gone. - The MemoryDb classes are deprecated in favour of the main Db classes. - The Memory class is deprecated. You set update_memory_on_run=True on an Agent that has a db. The fix: before and after v1 (breaks): from agno.agent import Agent from agno.memory.v2.db.sqlite import SqliteMemoryDb from agno.memory.v2.memory import Memory from agno.storage.sqlite import SqliteStorage memory = Memory(db=SqliteMemoryDb(table_name="user_memories", db_file="agno.db")) storage = SqliteStorage(table_name="agent_sessions", db_file="agno.db") agent = Agent(memory=memory, storage=storage, enable_user_memories=True) v2+ (ran on 3.1.10): from agno.agent import Agent from agno.db.sqlite import SqliteDb db = SqliteDb(db_file="agno.db") # sessions AND memories agent = Agent(db=db, update_memory_on_run=True) print(agent.get_user_memories(user_id="john@example.com")) # [] until a run stores something Things to check after the swap: 1. Pass user_id on every run. Memories are stored per user, so a missing or changing user_id looks like "memory isn't stored". 2. Table names. If you need your old table names, SqliteDb takes session_table= and memory_table= instead of table_name=. 3. Custom memory model or instructions. Build a MemoryManager (from agno.memory import MemoryManager) and pass memory_manager= to the Agent. The parameter names I found on the current Agent are memory_manager, enable_agentic_memory, update_memory_on_run and add_memories_to_context. 4. Existing v1 data. The migration guide links a v1-to-v2 migration script. It says to use the script from the v2 release matching your environment and not to run it under Agno v3, so migrate to v2 first, verify, then upgrade. Back up your database before running anything against it. 5. TypeError: UserMemory.__init__() got an unexpected keyword argument 'summaries'. This is a different, older error from mixed v1 memory data (#2955, Agno 1.4.1). The reporter confirmed it was gone after the maintainers' follow-up, so upgrade first. If you get ImportError: The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed when importing agno.db.sqlite, run pip install greenlet. I hit this on a clean Python 3.13 venv. If you want memories outside the local database update_memory_on_run writes to whatever Db you pass. If you want sessions to stay local but user memories to live in a hosted store shared across instances, MemorySync's agno-memorysync package backs Agno's MemoryManager (guide). The guide says it needs agno 2.8+, and this constructed fine on 3.1.10: from agno.agent import Agent from agno.db.sqlite import SqliteDb from agno.memory import MemoryManager from agno_memorysync import MemorySyncDb # pip install agno-memorysync; needs MEMORYSYNC_API_KEY agent = Agent( db=SqliteDb(db_file="agent.db"), # sessions: local memory_manager=MemoryManager(db=MemorySyncDb()), # memories: hosted update_memory_on_run=True, user_id="customer-42", ) The guide notes that MemorySyncDb is memory-only on purpose. Pointing sessions at it raises MemorySyncMemoryOnlyError, so keep db= local. Common questions Is there a compatibility shim for agno.memory.v2? Not in 3.1.10. The import fails. Update the imports. Do I still need a separate storage object? No. One Db instance handles sessions, memories, metrics, evals and knowledge in separate tables. My memories are stored but never show up in replies. If you pass a verbatim system_message=, it replaces Agno's built system prompt, which is where memories are injected. Use instructions= instead. (This tip is from MemorySync's Agno guide and applies to any memory db.) Which Agno version is current? PyPI served 3.1.10 on Oct 25, 2026. The v2 migration steps above still applied to it. To see a stored fact survive a reload before wiring anything in: try MemorySync in your browser. No signup is needed. Sources - agno-agi/agno#5017 (the import error): agno#5017 - agno-agi/agno#2955 (summaries TypeError): agno#2955 - Agno v2 migration guide: docs.agno.com - Agno, Agent with Memory: docs.agno.com - MemorySync Agno guide: docs.memorysync.io
The hard part is not creating an agent. It is operating one reliably. agno (~43k★): Build and manage AI agents in Python. agno-agi/agno What would you automate with a reliable agent?
Reduce API surface for BLAKE2 to match OpenSSL semantics This is an updated version of the original post which reduces the scope of the work on BLAKE2. I originally wanted to remove non-standard parameters to BLAKE2 to make the module more maintainable in the future as well as SIMD support, but I am planning to only remove SIMD support for now. I still think it's wrong to use tree-hashing with BLAKE2 since BLAKE3 exists and should be used instead. Nonetheless, after looking at the deprecation PRs (gh-160374 and gh-160375) and the long period this would require to actually remove the code, I don't think it's really worth it. It would at least reduce the impact to downstream users almost to zero. Instead, I suggest that we soft-deprecate the interface for tree hashing in the docs and suggest BLAKE3 instead. ---- Original post: I want to reduce BLAKE2 in hashlib to a minimal API matching OpenSSL. More precisely, BLAKE2 is kept without SIMD and matches the API of OpenSSL's BLAKE2 (digest, MAC): data and digest_size for the hash, and salt/person with a key. Tree hashing, salt/person without a key, and HMAC-BLAKE2 will be deprecated in 3.16 and removed in 3.21. If necessary, the deprecation period will be extended to a few more releases but I really want to get rid of the annoying API surface. BLAKE2 is already a keyed hash function, it doesn't make sense to use HMAC on top of it, despite some libraries using the HMAC-BLAKE2 HACL* sources for that (see gh-138114). In an ideal world, I would remove BLAKE2 and recommend BLAKE3 or SHA-based alternatives. Note that currently (and likely in the future), there are no plans to add BLAKE3 to hashlib (see gh-83479). For a fast secure hash, BLAKE3 is available as the blake3 package. It is maintained by the BLAKE3 authors, has a hashlib-like API, supports keyed hashing and key derivation, and has wheels for all major platforms. Maintenance pain BLAKE2 has been in hashlib since 3.6, first as a custom module with our own implementation, then as a wrapper around the vendored HACL* sources (support for HACL* HMAC was added in 3.14, together with support for BLAKE2 despite numerous shortcomings on some platforms). BLAKE2 has the largest API in hashlib (key, salt, person and seven tree-hashing parameters). Most of its maintenance cost comes from the SIMD code: it needs per-platform compiler flags and CPU detection as well as dedicated code paths for the vectorized versions, is only enabled on (some) x86-64, and usually breaks on non-Tier-1 platforms: - gh-123748: macOS universal2 builds failed (SIMD256 compiles for x86-64 but not ARM64); - gh-124304: SIMD had to be disabled on older Android versions; - gh-126458: SIMD support wrongly detected under WASI; - gh-128134: missing aligned_alloc check on older macOS; - gh-130213: older toolchains failed without the SIMD flags; - gh-133042: duplicate symbols when cross-compiling for Emscripten (still unfixed); - gh-138114: HMAC-BLAKE2 does not properly use the SIMD implementations (still unfixed). The module also needed several correctness fixes (gh-152851, gh-155830, gh-155835). Impact GitHub code search (excluding forks, CPython copies and vendored site-packages) finds about 34,000 Python files using hashlib.blake2b, 4,800 using hashlib.blake2s, and 300 using hashlib.new("blake2b") or hashlib.new("blake2s"). 62 of the 500 most-starred active Python repositories on GitHub use BLAKE2, e.g. PyTorch, Transformers, Home Assistant, Sentry, Streamlit, Gradio and Airflow. Of 1,753 sampled call sites (1,420 repositories), 1,649 (1,336 repositories) only use digest_size, 59 (51 repositories) use key, and 45 (38 repositories) use salt or person without a key. None use tree hashing. Most of them only need a fast hash, for which SHA-2, SHA-3 or BLAKE3 also work. This includes: - projects that store digests, such as Home Assistant (cache filenames), Mathesar (database identifiers) and Onyx (search-index IDs); - projects that depend on the exact BLAKE2 output, such as networkx (graph hashes), LangChain (indexing), apprise and a minisign verifier (BLAKE2b-512 signatures), copyparty (client checksums); - projects that use keyed BLAKE2 such as agno (MinHash) and openai-python (Bedrock helper); or - projects such as PyPI or Poetry for checksums, Warehouse for storage and publishing (though it looks like a legacy API), or python-tuf. One popular project affected by this change would be ComfyUI (which uses person without a key). In particular, I don't expect too much of downstream breakage (considering the long deprecation period) but I would appreciate if anyone against this proposal could explain why SIMD BLAKE2 should be kept (note that I already plan to remove SIMD support in 3.16 without a deprecation notice as it created too many issues, while exposing the OpenSSL implementation). Note that projects can rely on nacl.hash.blake2b (PyNaCl does not offer a 2s version) or PyCryptodome for blake2b and blake2s. AFAIK, there is no PyPI packages providing the tree-hashing feature but the latter is neither standardized by an RFC nor used in the wild. ---- > AI disclosure: Opus 5.5 was used for collecting and classifying data.
Deploying Agno agents to production as a REST API Originally published on hivegate.dev. An Agno agent that works in a script is a few lines of Python. Turning it into something a web or mobile app can call for thousands of users takes more: an HTTP endpoint, a conversation per user, history that survives restarts, API keys, model keys kept on the server, and a container you can deploy. This post shows how HiveGate, an MIT-licensed FastAPI service built on Agno, handles each of those. It also covers when you should skip HiveGate and use Agno's own AgentOS. First: you may not need HiveGate Agno ships AgentOS, which serves agents defined in code over HTTP, with sessions, human approval, RBAC and tracing: from agno.agent import Agent from agno.db.postgres import PostgresDb from agno.models.google import Gemini from agno.os import AgentOS support_agent = Agent( name="support-agent", model=Gemini(id="gemini-3-flash-preview"), db=PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5432/ai"), add_history_to_context=True, ) app = AgentOS(agents=[support_agent]).get_app() If your agents live in your codebase and that covers what you need, use AgentOS. It's maintained by the Agno team and tracks Agno releases. HiveGate is for a different case: agents that act for your users in their own Google or Microsoft accounts, and agents, prompts and skills that you manage as data through an API instead of shipping code for each change. Agents are records, not code In HiveGate you create an agent with a POST. The template becomes the agent's system prompt, stored as a versioned prompt in Postgres. The config block maps onto Agno's Agent options. curl -X POST http://localhost:8000/v2/agents \ -H "X-API-Key: $HIVEGATE_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "support-agent", "name": "support-agent", "template": "You are the support assistant for Acme. Answer briefly and cite the help center when you can.", "description": "Customer support for Acme", "tags": ["available_tools:email,calendar"], "config": { "enable_memory": true, "enable_history": true, "num_history_runs": 3 } }' The available_tools: tag switches on HiveGate's built-in toolkits for this agent. Leave it out and the agent gets none of them. Agents that need other tools can point at MCP servers over Streamable HTTP through config.worker_config.mcp_servers. Changing the prompt later is a PUT /v2/prompts/support_agent (the prompt ID is the agent's name in snake case). The next chat picks up the new template, so a prompt fix doesn't need a redeploy. Chatting, with sessions per user Every chat request names a user_id and a session_id. Both are required, as are timezone and locale: curl -X POST http://localhost:8000/v2/agents/support-agent/chat \ -H "X-API-Key: $HIVEGATE_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "My invoice from last month looks wrong.", "user_id": "user-1842", "session_id": "ticket-5521", "timezone": "America/New_York", "locale": "en-US", "stream": false }' The non-streaming response is a JSON object with content, session_id, run_id, status, the tools the agent called, and token_usage for that call. Here is what happens to those two IDs: - History. Agno stores each run in a Postgres session table, one table per agent. With enable_history on, the last num_history_runs runs of the same session go back into the model's context. A follow-up with the same session_id continues the conversation, even after a restart or on another replica. - Memory. With enable_memory on, Agno's memory manager keeps facts about the user across sessions, keyed by user_id. - Agent cache. HiveGate builds the Agno Agent once per (prompt, agent, model, user, session) and caches it in the process, so later turns skip setup. Per-request context goes into Agno's session_state. HiveGate fills it from the request: the user's name and profile (from user_profile), timezone and locale. If you send a tenant_profile, knowledge search is filtered to that tenant's documents, so one deployment can serve several customers without mixing their data. Streaming stream defaults to true. The response is Server-Sent Events, one event: message per Agno event, each with a JSON data line: import json import httpx payload = { "message": "Summarize my open tickets.", "user_id": "user-1842", "session_id": "ticket-5521", "timezone": "UTC", "locale": "en", } with httpx.stream( "POST", "http://localhost:8000/v2/agents/support-agent/chat", headers={"X-API-Key": HIVEGATE_KEY}, json=payload, timeout=120, ) as response: for line in response.iter_lines(): if not line.startswith("data: "): continue event = json.loads(line[len("data: "):]) if event.get("event") == "RunContent" and isinstance(event.get("content"), str): print(event["content"], end="", flush=True) If a model call fails mid-stream, you get an event: error with a generic message. The details stay in the server log, so provider internals don't leak to clients. Picking the model per request A request can set model. Otherwise HiveGate uses DEFAULT_CHAT_MODEL, and falls back to gemini-3-flash-preview if that's unset. Besides pinned IDs such as claude-sonnet-4-6, it accepts aliases like anthropic:sonnet-latest or google:flash-latest. An alias resolves to the vendor's newest model in that tier and never moves you up to a pricier tier. Model keys stay in the server's environment; clients never see them. API keys and cost per user /v2/* requires an X-API-Key. Create keys with the admin secret: curl -X POST http://localhost:8000/admin/api-keys \ -H "X-Admin-Secret: $ADMIN_SECRET" \ -H "Content-Type: application/json" \ -d '{"name": "acme-web", "rate_limit": 1000}' The raw key is in the api_key field of the response, and it's shown only once. For local work, AUTH_DISABLED=true skips the check. Never set it on a deployment other people can reach. Every model call is metered: tokens, cache reads, tool calls, duration and USD cost go into a token_usage table, including failed and abandoned runs. To see which users cost the most: curl -H "X-API-Key: $USAGE_KEY" \ "http://localhost:8000/usage/summary?window=7d&group_by=user" The usage endpoints accept a key with the usage:read scope. A key whose only scope is usage:read is refused on /v2, so a dashboard key can't call agents. Running it Locally, Docker Compose starts Postgres and Qdrant and seeds demo agents: git clone https://github.com/hivegate-ai/hivegate && cd hivegate cp .env.example .env # set GOOGLE_API_KEY, uncomment AUTH_DISABLED=true docker compose up -d ./scripts/dev_setup.sh && source .venv/bin/activate && ./scripts/start_server.sh For a server, use the prebuilt multi-arch image ghcr.io/hivegate-ai/hivegate:latest. The deploy page has one-click buttons for Render, Railway and Koyeb, plus Kubernetes and AWS, Azure and GCP manifests. The settings that matter: | Variable | Why | |---|---| | DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_DATABASE | Postgres for agents, sessions, tokens and usage | | ADMIN_SECRET | Protects /admin/*, where API keys are created | | GOOGLE_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY | At least one model provider | | SECRET_TOKEN_ENC_KEY | Fernet key that encrypts stored user OAuth tokens | | QDRANT_URL | Optional, for per-tenant knowledge | GET /health is the health check. The deploy page ends with a five-step smoke test: health, list agents, create an agent, one real LLM call, then cleanup. What to know before scaling out Conversation history is in Postgres, so any replica can serve the next turn. Two things live in process memory: the cached Agent objects, and runs that are paused waiting for a human to approve a tool call. A paused run has to be resumed on the process that paused it. If you use approvals, run one replica or route each session to the same instance. The post on approvals covers that flow. When to use which - Agents defined in code, one team, standard Agno features: AgentOS. - Agents that act in each user's Gmail, Outlook or calendar; agents and prompts edited through an API; per-tenant knowledge; supervisor teams with container workers: HiveGate. Try it - Deploy in one click from the deploy page. - Read the source, open an issue or star the repo on GitHub. If this is useful, a star helps other people find the project: ⭐ github.com/hivegate-ai/hivegate.
想让 AI Agent 调工具,还能查看运行记录?🤖 Agno 提供 Python Agent 框架和 AgentOS,方便连接工具、部署及追踪运行。🧩 agno-agi/agno #AIAgent #Python #浇朋友 #蓝朋友 #互关 #fo
Oct 6: Agno (still open) A queue with zero doors doesn't need a longer timeout. It needs a door. I opened a fix to reject zero/negative concurrency in two page fetchers before requests can wait forever. Local checks; PR open. agno#10847
Oct 6 rewind (2026): Agno A failed save shouldn't take the last good save down with it. I staged regular JSON tables before replacing them. Direct-write fallbacks still remain. Tiny files, big trust issues. Submitted Oct 3; merged Oct 6. agno#10794