Skip to content

Documentation navigation

Agents & Tools

Three ways to register tools, cross-agent dependencies, before/after hooks, and datetime awareness.

#Agents & Tools

This page covers the three ways to attach tools to an agent, plus the dependency, hook, and datetime-awareness mechanics that build on top of them.

#Tool registration: three equivalent forms

#1. Decorator — @agent.toolify(...)

The most common form. Define and register in one step:

Python
agent = Agent(name='Support Planner', system_prompt='...', api_key='...')

@agent.toolify(name='fetch_device_telemetry')
def fetch_device_telemetry(device_id: str) -> dict:
    return {'device_id': device_id, 'temperature_c': 84.2, 'error_rate': 0.17}

#2. Imperative — agent.add_tool(...)

When you want to define tools elsewhere and register them on an existing agent (useful for libraries of reusable tools):

Python
def fetch_device_telemetry(device_id: str) -> dict:
    return {'device_id': device_id, 'temperature_c': 84.2}

def triage_device_issue(telemetry: dict) -> dict:
    return {'priority': 'high' if telemetry['temperature_c'] > 80 else 'normal'}

class ResolutionPlan(BaseModel):
    device_id: str
    priority: str
    recommended_action: str

agent = Agent(name='Support Planner', system_prompt='...', api_key='...')

agent.add_tool(fetch_device_telemetry, tags=['telemetry'])
agent.add_tool(triage_device_issue, name='triage_device_issue')
agent.add_tool(ResolutionPlan, name='write_resolution_plan', final_tool=True)

#3. Constructor — Agent(..., tools=[...])

When the tool list is known up front and you'd rather express it in one place:

Python
def load_account_profile(account_id: str) -> dict:
    return {'account_id': account_id, 'tier': 'enterprise', 'renewal_days': 42}

def load_product_usage(account_id: str) -> dict:
    return {'active_users': 118, 'support_tickets': 3}

@depends_on_tool(load_account_profile, arg_name='profile')
@depends_on_tool(load_product_usage, arg_name='usage')
def build_account_brief(profile: dict, usage: dict) -> dict:
    return {
        'account': profile['account_id'],
        'health': 'green' if usage['support_tickets'] <= 3 else 'yellow',
    }

agent = Agent(
    name='Account Success Agent',
    system_prompt='...',
    api_key='...',
    tools=[load_account_profile, load_product_usage, build_account_brief],
)

All three forms produce the same registered tools. Pick whichever fits your code organization.

#Listing what's registered

Python
for tool in agent.list_tools():
    print(f'{tool.name}: {tool.description}')

Useful inside a CLI or admin route to introspect what an agent can do.

#Cross-agent dependencies

A tool can declare that calling it implicitly invokes another agent and receives that agent's output. This is @depends_on_agent:

Python
from maivn import Agent, depends_on_agent

data_analyzer = Agent(
    name='Data Analyzer',
    system_prompt='You analyze datasets with the analyze_dataset tool.',
    api_key='...',
)

@data_analyzer.toolify(name='analyze_dataset')
def analyze_dataset(dataset_name: str) -> dict:
    return {'dataset': dataset_name, 'mean': 42.5, 'stddev': 12.3, 'sample_size': 1000}

research_coordinator = Agent(
    name='Research Coordinator',
    system_prompt='Coordinate research. Call generate_research_report at the end.',
    api_key='...',
)

@depends_on_agent(data_analyzer, arg_name='analysis_result')
@research_coordinator.toolify(name='generate_research_report')
class ResearchReport(BaseModel):
    title: str
    dataset_info: dict
    analysis_result: dict  # automatically injected from data_analyzer's output
    conclusions: list[str]

When generate_research_report is constructed, the runtime invokes data_analyzer first and passes its final result in as analysis_result. The coordinator never has to know how the analyzer works. It just sees a field that gets filled in.

@depends_on_agent(agent_ref, arg_name='...') declares that the upstream agent is invoked and its output injected into the named argument.

#Tool execution hooks

before_execute and after_execute callbacks fire around every tool invocation. They are useful for audit logging, metrics, or correlation IDs.

#Per-tool hooks

Pass them through toolify:

Python
def log_before(payload: dict) -> None:
    print(f'[BEFORE] tool={payload["tool"].name} args={payload.get("arguments")}')

def log_after(payload: dict) -> None:
    if payload.get('error'):
        print(f'[AFTER] tool={payload["tool"].name} FAILED: {payload["error"]}')
    else:
        print(f'[AFTER] tool={payload["tool"].name} ok -> {payload.get("result")}')

@agent.toolify(
    name='extract_ticket',
    before_execute=log_before,
    after_execute=log_after,
)
def extract_ticket(ticket: str) -> dict:
    if 'ERROR' in ticket.upper():
        raise RuntimeError('ticket contains ERROR')
    return {'customer': 'Acme Co', 'priority': 'P2'}

#Scope-level hooks (every tool the agent or swarm runs)

Set before_execute / after_execute directly on the Agent or Swarm to hook every tool that runs in that scope:

Python
agent.before_execute = log_before
agent.after_execute = log_after

# Or on a swarm — fires for every tool every member agent runs.
swarm.before_execute = log_before
swarm.after_execute = log_after

#Tuning when scope-level hooks fire

hook_execution_mode controls how often the scope-level hooks run:

  • "tool" (default) — once per tool execution.
  • "scope" — once per invocation of this scope.
  • "agent" (swarm only) — once per agent execution inside the swarm.
Python
swarm.hook_execution_mode = 'agent'   # one fire per agent run, not per tool

The hook payload includes:

KeyMeaning
stage"before" or "after"
toolThe tool descriptor (.name, .description, …)
tool_idTool identifier supplied to the hook
argumentsResolved arguments about to be passed in
resultThe tool's return value (after only)
errorThe exception, if the tool raised (after only)

Hook arguments and results can contain application data. Filter or redact them before writing them to logs or external telemetry.

Hooks that fire are surfaced inline in mAIvn Studio on the owning card (tool / agent / swarm) so you can verify behavior without tailing logs.

#Datetime awareness

By default, the LLM has no idea what time or timezone it is. Configure the Client once and every invocation receives a timestamp + zone:

Python
from maivn import Agent, Client

client = Client(api_key='...', client_timezone='America/New_York')

agent = Agent(name='Scheduler Assistant', system_prompt='...', client=client)
agent.invoke([HumanMessage(content='What does my afternoon look like?')])

The agent sees a system-injected timestamp in RFC 3339 format with the zone name, so prompts like "next Tuesday" or "this afternoon" resolve correctly.

#What's next

  • Swarms — multiple agents working together.
  • Tools guide — full decorator reference.
  • Private Data — dependency injection for sensitive values.