Agno Adapter¶
The AgnoAdapter produces a wrap-around hook function compatible with Agno's
tool_hooks parameter. Unlike other adapters, the Agno hook wraps the entire
tool execution -- it receives the callable and is responsible for invoking it.
When to use this¶
Add Edictum to your Agno agent when you need to enforce contracts on tool calls. The as_tool_hook() method returns a wrap-around function that integrates with Agno's tool_hooks parameter, applying preconditions and postconditions to every tool invocation. Because the hook wraps the entire tool execution, it can both deny calls and replace tool results -- making the on_postcondition_warn callback suitable for PII redaction in regulated environments.
Installation¶
Integration¶
from edictum import Edictum
from edictum.adapters.agno import AgnoAdapter
from agno.agent import Agent
guard = Edictum.from_yaml("contracts.yaml")
adapter = AgnoAdapter(guard=guard)
hook = adapter.as_tool_hook()
agent = Agent(
model="gpt-4o-mini",
tools=[search_tool, file_tool],
tool_hooks=[hook],
)
The hook is passed via the tool_hooks parameter on the Agent constructor.
It intercepts every tool call made by the agent.
Hook Behavior¶
The hook function receives three arguments:
The adapter controls the full lifecycle:
- Evaluates preconditions against the tool name and arguments.
- If allowed, calls
function_call(**arguments)to execute the tool. - Evaluates postconditions against the result.
- Returns the tool result on success or
"DENIED: <reason>"on deny.
Because the adapter wraps the entire call, it can both block execution and replace the returned result.
PII Redaction Callback¶
Use on_postcondition_warn to transform tool output when postconditions flag
issues. Because this is a wrap-around adapter, the callback's return value
replaces the tool result:
import re
def redact_pii(result, findings):
text = str(result)
text = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[SSN REDACTED]", text)
text = re.sub(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b", "[EMAIL REDACTED]", text)
return text
hook = adapter.as_tool_hook(on_postcondition_warn=redact_pii)
Known Limitations¶
-
Kwargs spread: The adapter calls
function_call(**arguments), spreading the arguments dict as keyword arguments. Your tool callables must accept keyword arguments, not a single positional dict. -
Async-to-sync bridging: Agno hooks are synchronous, but Edictum's pipeline is async. When no event loop is running, the adapter uses
asyncio.run(). When a loop is already running (common in async frameworks), it spins up aThreadPoolExecutorwith a single worker to run the async code in a fresh event loop. Objects with thread affinity (e.g., some DB connections) may not transfer correctly across this boundary. -
Async tool support: If
function_call(**arguments)returns a coroutine, the adapter awaits it automatically.
Full Working Example¶
from edictum import Edictum, Principal
from edictum.adapters.agno import AgnoAdapter
from agno.agent import Agent
# Load contracts
guard = Edictum.from_yaml("contracts.yaml")
adapter = AgnoAdapter(
guard=guard,
session_id="agno-session-01",
principal=Principal(user_id="research-agent", role="analyst"),
)
hook = adapter.as_tool_hook()
# Define tools
def search_documents(query: str) -> str:
"""Search the document store."""
return f"Found 3 results for: {query}"
def read_file(path: str) -> str:
"""Read a file from disk."""
with open(path) as f:
return f.read()
# Build agent with contract enforcement
agent = Agent(
model="gpt-4o-mini",
tools=[search_documents, read_file],
tool_hooks=[hook],
)
result = agent.run("Search for quarterly earnings data")
print(result)
Observe Mode¶
Deploy contracts without enforcement to see what would be denied:
guard = Edictum.from_yaml("contracts.yaml", mode="observe")
adapter = AgnoAdapter(guard=guard)
hook = adapter.as_tool_hook()
In observe mode, the hook always allows tool calls through, even for calls that
would be denied. CALL_WOULD_DENY audit events are emitted so you can review
enforcement behavior before enabling it.