Best Practices
Production-grade patterns for robust agent behavior.
#Best Practices
Privacy controls are not certifications. Redaction, private-data injection, and phi_mode are technical controls. mAIvn holds no SOC 2, HIPAA, ISO 27001, or FedRAMP certification; these controls do not provide a BAA or replace required audits and organizational safeguards. Evaluate and use them at your own risk under the mAIvn Platform Disclaimer.Use these patterns when preparing mAIvn agents for production. Each section covers a design or operational concern, from tool permissions to error handling.
#Agent Design
#Clear Naming
Use descriptive names that reflect purpose:
# Good
agent = Agent(
name='customer_support_agent',
description='Handles customer inquiries and support tickets',
api_key='...',
)
# Avoid
agent = Agent(name='agent1', api_key='...')#Effective System Prompts
Write system prompts that guide behavior:
agent = Agent(
name='data_analyst',
system_prompt='''You are a data analyst assistant.
Your capabilities:
- Analyze datasets using the analyze_data tool
- Generate reports using the create_report tool
- Answer questions about data trends
Guidelines:
- Always validate data before analysis
- Explain your methodology clearly
- Provide confidence levels for predictions
- Use the final_report tool for all final outputs''',
api_key='...',
)#Focused Agents
Each agent should have a clear, focused responsibility:
# Good: focused agents
researcher = Agent(name='researcher', description='Finds information', api_key='...')
analyst = Agent(name='analyst', description='Analyzes data', api_key='...')
writer = Agent(name='writer', description='Writes content', api_key='...')
# Avoid: unfocused agent
general = Agent(name='everything', description='Does all tasks', api_key='...')#Tool Design
#Choose the Registration Style That Fits the Module
Use @agent.toolify(...) when the tool naturally belongs next to the agent definition:
@agent.toolify(description='Fetch customer orders')
def get_orders(customer_id: str) -> dict:
...Use Agent(..., tools=[...]) when tools are already defined and you want the agent configuration to show its complete tool surface:
def get_orders(customer_id: str) -> dict:
"""Fetch customer orders."""
...
agent = Agent(
name='orders',
api_key='...',
tools=[get_orders],
)Use agent.add_tool(...) when tools are assembled conditionally or imported from another module:
agent = Agent(name='orders', api_key='...')
agent.add_tool(get_orders, description='Fetch customer orders')All three styles use the same underlying tool registry and dependency decorators.
#Clear Descriptions
Tool descriptions help the LLM decide when to use them:
# Good: specific and actionable
@agent.toolify(description='Search product catalog by name, SKU, or category')
def search_products(query: str) -> dict: ...
# Avoid: vague
@agent.toolify(description='Search')
def search_products(query: str) -> dict: ...#Type Hints and Defaults
Use specific types and sensible defaults:
from typing import Literal
from pydantic import Field
@agent.toolify(description='Fetch customer orders')
def get_orders(
customer_id: str,
status: Literal['pending', 'shipped', 'delivered'] | None = None,
limit: int = Field(default=10, ge=1, le=100),
) -> dict:
...#Focused Tools
One tool should do one thing well:
# Good: separate focused tools
@agent.toolify()
def fetch_user(user_id: str) -> dict: ...
@agent.toolify()
def update_user(user_id: str, data: dict) -> dict: ...
@agent.toolify()
def delete_user(user_id: str) -> dict: ...
# Avoid: multi-purpose tool
@agent.toolify()
def manage_user(action: str, user_id: str, data: dict | None = None) -> dict: ...#Agent-Ready Toolsets
A toolset should be useful when a developer registers it directly and asks the agent for a normal workflow:
agent.add_toolset(MyToolSet(...))If a toolset only works after custom DAG construction, long prompt engineering, or demo-specific wrappers, treat that as an interface problem. Move the needed guidance into the tool names, descriptions, parameter schemas, defaults, permission markers, and return values.
For provider-style toolsets:
- Use
@toolsetfor related operations that share state, credentials, or an HTTP client. - Put accurate
permissions=PermissionSet(...)on every@toolifymethod so callers can useinclude_tags=['read']andexclude_tags=['destructive']. - Mark irreversible tools with
destructive=True. - Return compact, human-readable summaries by default from broad list/search tools.
- Hide raw provider IDs by default unless another tool needs them; prefer an opt-in
include_ids=Falseargument. - Cap broad searches and expensive metadata expansion with small useful defaults.
- Let write tools accept natural read-tool outputs when the mapping is safe and obvious.
- Keep provider toolsets generic and register application wrappers for custom defaults, output handling, or dependency behavior.
#Human-Readable Tool Results
Agent final answers get worse when tool results are mostly internal handles. Prefer result shapes like this:
{
'tickets': [
{
'ticket_ref': 'ticket_1',
'title': 'Cannot sign in',
'requester': 'Ada Lovelace',
'status': 'open',
'updated_at': '2026-05-16T13:30:00Z',
}
],
'next_page_token': '...',
}Use raw IDs for follow-up tool calls, not as the main content in final responses. If a downstream update tool needs the provider ID, expose it only when requested:
def search_tickets(query: str, *, limit: int = 10, include_ids: bool = False) -> dict:
...#Error Returns
Return structured errors instead of raising exceptions:
@agent.toolify()
def fetch_data(source: str) -> dict:
try:
data = external_api.fetch(source)
return {'status': 'success', 'data': data}
except NotFoundError:
return {'status': 'error', 'error': f'Source not found: {source}'}
except PermissionError:
return {'status': 'error', 'error': 'Permission denied'}#Dependency Management
#Parallel Where Possible
Design for parallel execution:
# Good: independent tools run in parallel
@agent.toolify()
def fetch_users() -> dict: ...
@agent.toolify()
def fetch_products() -> dict: ...
@agent.toolify()
@depends_on_tool(fetch_users, 'users')
@depends_on_tool(fetch_products, 'products') # Both run in parallel
def generate_report(users: dict, products: dict) -> dict: ...#Orchestration Policy
#Make Required Steps Explicit
Declare required tool and agent dependencies rather than relying on a policy name to imply another execution cycle. For repair or verification workflows, put the acceptance check in a tool or in your application and start another invocation when the observed result requires it.
SessionOrchestrationConfig is retained as a compatibility schema, but the current SDK does not send it to the runtime. Its mode, stop_strategy, and max_cycles fields do not currently change execution. See the Session Config reference.
For swarms, use_as_final_output=True identifies the member that produces the user-facing response. Do not assume it also creates an autonomous repair loop.
#Security
#Private Data for Secrets
Never hardcode secrets:
# Good: use private data
agent.private_data = {'api_key': os.environ['API_KEY']}
@agent.toolify()
@depends_on_private_data(data_key='api_key', arg_name='key')
def call_api(key: str) -> dict:
return {"status": "ok"}
# Avoid: hardcoded secrets
@agent.toolify()
def call_api() -> dict:
api_key = 'sk-xxx-hardcoded' # Never do this!
return {"api_key": api_key}#Minimal Private Data
Only include what's needed:
# Good: only what's needed
agent.private_data = {
'db_connection_string': '...', # Used by database tool
}
# Avoid: everything "just in case"
agent.private_data = {
'db_connection_string': '...',
'unused_secret_1': '...',
'unused_secret_2': '...',
}#PII Whitelist: Use Sparingly, Justify Always
PIIWhitelist lets a RedactedMessage opt out of redacting specific spans (public URLs, your own published support address, etc.). Treat it as a narrow exception, not a default:
from maivn import PIIWhitelist, PIIWhitelistEntry, RedactedMessage
# Good: narrow value entry, real justification, PHI-safe.
RedactedMessage(
content='Reach us at support@maivn.io.',
pii_whitelist=PIIWhitelist(
entries=(
PIIWhitelistEntry(
value='support@maivn.io',
justification='Public support address listed on docs site.',
),
),
phi_mode=False,
),
)
# Avoid: broad entity_type whitelist with weak justification.
PIIWhitelist(
entries=(
PIIWhitelistEntry(entity_type='email', justification='easier'),
),
)#Always Set phi_mode=True in PHI Deployments
If your deployment ever handles Protected Health Information, set phi_mode=True on every PIIWhitelist. The validator will refuse any entity-type whitelist for HIPAA Safe Harbor categories at construction time:
from maivn import HIPAA_SAFE_HARBOR_CATEGORIES, PIIWhitelist, PIIWhitelistEntry
# Validate at boot rather than at request time.
def build_whitelist(entries: list[PIIWhitelistEntry]) -> PIIWhitelist:
for entry in entries:
if entry.entity_type and entry.entity_type in HIPAA_SAFE_HARBOR_CATEGORIES:
raise ValueError(
f"Refusing PHI-unsafe whitelist entry: {entry.entity_type}"
)
return PIIWhitelist(entries=tuple(entries), phi_mode=True)#Always Provide a Real justification
The justification is required (at least 8 characters) and records why a span was deliberately allowed. Write a concrete reason and retain any separate review or approval evidence required by your organization.
#Match EventBridge Audience to the Frontend
Treat browser event streams as a trust boundary:
from maivn.events import EventBridge
# End-user browser UI
public_bridge = EventBridge('session-1', audience='frontend_safe')
# Internal developer/admin UI
internal_bridge = EventBridge('session-1', audience='internal')Use frontend_safe when the event stream reaches end users. Use internal only for trusted tooling such as mAIvn Studio or your own internal operator consoles.
For the easiest possible end-to-end setup, use the FastAPI adapter: one line wires GET /maivn/events/{session_id} and your frontend (in any language) can consume it via EventSource or any SSE client:
from fastapi import FastAPI
from maivn.events.fastapi import mount_events
app = FastAPI()
mount_events(app, factory=lambda sid: EventBridge(sid, audience='frontend_safe'))See the frontend events guide for client examples in JavaScript, TypeScript, Swift, Kotlin, Go, Python, Rust, and more.
#Harden Third-Party stdio MCP Servers
If you launch third-party MCP servers over stdio, prefer explicit environment passing instead of inheriting your full backend environment:
mcp_server = MCPServer(
name='external_tools',
transport='stdio',
command='python',
args=['-m', 'my_mcp_server'],
inherit_env_allowlist=['OPENAI_API_KEY'],
env={'SERVICE_TOKEN': 'explicit-token'},
stdio_response_timeout_seconds=30,
)The SDK defaults to a minimal subprocess environment. Set inherit_env=True only when you trust the MCP server and it genuinely needs the full parent shell environment.
#Error Handling
#Graceful Degradation
Handle errors without crashing:
@agent.toolify()
def fetch_with_fallback(primary_source: str) -> dict:
try:
return {'data': fetch_from_primary(primary_source)}
except PrimarySourceError:
try:
return {'data': fetch_from_backup(), 'source': 'backup'}
except BackupError:
return {'error': 'All sources unavailable'}#Informative Errors
Return errors that help debugging:
@agent.toolify()
def process_data(data: dict) -> dict:
if 'required_field' not in data:
return {
'error': 'Missing required_field',
'received_fields': list(data.keys()),
}
return {"status": "processed"}#Performance
#Timeout Configuration
Set the HTTP timeout for the expected response cadence:
client = Client(
api_key='...',
http_timeout=120,
)This controls HTTP transport waits, not the total run duration. The compatibility fields tool_execution_timeout, dependency_wait_timeout, and total_execution_timeout do not enforce execution deadlines. Use client.cancel(session_id) to request cancellation of an active run; see Reliability.
#Limit Results
max_results is a compatibility field with no effect on v2 tool selection. Keep catalogs focused by registering only relevant tools, filtering toolsets, or using targeted_tools for a specific call. Limit broad search results in the tool function itself rather than relying on the agent's max_results setting.
#Resource Cleanup
Clean up resources when done:
agent = Agent(name='temp', api_key='...')
try:
response = agent.invoke([...])
finally:
for server in agent.list_mcp_servers():
server.close()
agent.client.close() # This example owns its client.#Testing
Because a tool is just your function, the bulk of your coverage is ordinary Python. Call the function directly, with no agent or network involved:
def test_fetch_data():
result = fetch_data('test-source')
assert result['status'] == 'success'
assert 'data' in resultFor inspecting compiled tool metadata offline (compile_tools()), mocking agent responses and streams, and gating live tests behind a marker, see the dedicated Testing & Local Development guide.
#Logging
#Enable in Development
from maivn import configure_logging
logger = configure_logging('logs/dev.log')
logger.setLevel('DEBUG')#Use Event Builder for Live Tracing
response = agent.events().invoke(messages)#Don't Log Secrets
# Good
logger.info(f'Processing request for user {user_id}')
# Avoid
logger.info(f'Using API key {api_key}') # Never log secrets!#Swarm Design
#Clear Responsibilities
Each agent in a swarm should have a distinct role:
swarm = Swarm(
name='content_team',
agents=[
Agent(name='researcher', description='Finds information', api_key='...'),
Agent(name='analyst', description='Analyzes findings', api_key='...'),
Agent(name='writer', description='Writes final content', api_key='...'),
],
)#Single Final Output
Designate exactly one source of final output:
# Option 1: final output agent
writer = Agent(name='writer', api_key='...', use_as_final_output=True)
# Option 2: swarm-level final tool
@swarm.toolify(final_tool=True)
class TeamReport(BaseModel): ...#Minimal Agent Count
Use the minimum number of agents needed:
# Good: 3 agents for clear separation of concerns
researcher -> analyst -> writer
# Avoid: unnecessary agents
researcher -> summarizer -> analyst -> formatter -> writer -> editor#See Also
- Troubleshooting - Solving common issues
- API Reference - Complete API documentation
- Guides - Step-by-step tutorials