Skip to content

Documentation navigation

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:

Python
# 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:

Python
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:

Python
# 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:

Python
@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:

Python
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:

Python
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:

Python
# 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:

Python
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:

Python
# 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:

Python
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 @toolset for related operations that share state, credentials, or an HTTP client.
  • Put accurate permissions=PermissionSet(...) on every @toolify method so callers can use include_tags=['read'] and exclude_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=False argument.
  • 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:

Python
{
    '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:

Python
def search_tickets(query: str, *, limit: int = 10, include_ids: bool = False) -> dict:
    ...

#Error Returns

Return structured errors instead of raising exceptions:

Python
@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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
@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:

Python
@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:

Python
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:

Python
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:

Python
def test_fetch_data():
    result = fetch_data('test-source')
    assert result['status'] == 'success'
    assert 'data' in result

For 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

Python
from maivn import configure_logging

logger = configure_logging('logs/dev.log')
logger.setLevel('DEBUG')

#Use Event Builder for Live Tracing

Python
response = agent.events().invoke(messages)

#Don't Log Secrets

Python
# 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:

Python
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:

Python
# 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:

Plain text
# Good: 3 agents for clear separation of concerns
researcher -> analyst -> writer

# Avoid: unnecessary agents
researcher -> summarizer -> analyst -> formatter -> writer -> editor

#See Also