Skip to content

Documentation navigation

Dependencies

Model tool and agent dependencies for reliable execution graphs.

#Dependencies Guide

Most real tasks have steps that depend on each other: fetch the data before you summarize it, get the user's confirmation before you charge their card. Dependencies are how you tell mAIvn about that wiring, so it runs each step in the right order and hands one step's result to the next automatically — no manual plumbing.

This guide walks through every dependency pattern, from chaining two tools together to building multi-stage graphs across agents.

#Overview

The mAIvn SDK supports four types of dependencies:

TypeDecoratorDescription
Tool@depends_on_toolOutput from another tool
Agent@depends_on_agentOutput from another agent
Private Data@depends_on_private_dataInject a secret at execution time
Interrupt@depends_on_interruptUser input collection

It also supports two execution controls:

ControlDecoratorDescription
Await-for@depends_on_await_forEnforce execution order without injecting data
Reevaluate@depends_on_reevaluateForce a reevaluate boundary before the dependent runs — the runtime guarantees it (not a soft hint)

#Tool Dependencies

Chain tools by injecting one tool's output into another.

#Basic Pattern

Python
from maivn import Agent, depends_on_tool

agent = Agent(name='data_agent', api_key='...')

@agent.toolify(description='Fetch raw data from source')
def fetch_data(source: str) -> dict:
    return {'source': source, 'records': [1, 2, 3]}

@agent.toolify(description='Process fetched data')
@depends_on_tool(fetch_data, arg_name='raw_data')
def process_data(raw_data: dict) -> dict:
    return {'processed': len(raw_data['records'])}

When process_data is called, fetch_data runs first and its output is passed to raw_data.

Dependency decorators are independent of the registration style. The same dependency graph can be registered through Agent(..., tools=[...]) or agent.add_tool(...):

Python
@depends_on_tool(fetch_data, arg_name='raw_data')
def process_data(raw_data: dict) -> dict:
    """Process fetched data."""
    return {'processed': len(raw_data['records'])}

agent = Agent(name='data_agent', api_key='...', tools=[fetch_data, process_data])

#Reusing a Stable Result on Later Turns

A follow-up is a new invocation. Tool results therefore stay within the invocation by default, even when the follow-up uses the same thread and message history. This avoids silently reusing a confirmation, authorization decision, or time-sensitive lookup.

For a stable producer result, opt in on the exact dependency edge that may reuse it:

Python
@agent.toolify(description='Load durable account context')
def load_account(account_id: str) -> dict:
    return {'account_id': account_id, 'plan': 'enterprise'}

@agent.toolify(description='Answer using the account context')
@depends_on_tool(
    load_account,
    arg_name='account',
    result_scope='conversation',
)
def answer_account_question(account: dict) -> dict:
    return {'account_id': account['account_id'], 'answer': '...'}

The default is result_scope='invocation'. Conversation-scoped reuse is available only when all of these checks pass:

  • the consumer's dependency explicitly declares result_scope='conversation'
  • the stored result came from a successful producer call in the same thread
  • the stored producer identity and spec match the dependency being resolved
  • the stored value is still available through the trusted restoration path

When those checks pass, the retained result can cross another live turn or an eligible Studio History Continue on that thread.

If the producer is called in the new invocation, the new attempt takes precedence, including a successful None result. If that refresh fails, the consumer fails rather than falling back to an older success. New human input also starts a fresh input round: an accepted newer answer supersedes an earlier answer, and a failed newer round cannot quietly reuse the older one.

This setting carries one declared result. It does not restore mutable Python objects, revive the earlier runtime, reuse every nested dependency, replay a tool call, or make side effects exactly-once. Code that performs actions should still use its normal authorization and idempotency controls on every invocation.

#Multiple Tool Dependencies

Python
@agent.toolify()
def fetch_users() -> dict:
    return {'users': [...]}

@agent.toolify()
def fetch_orders() -> dict:
    return {'orders': [...]}

@agent.toolify()
@depends_on_tool(fetch_users, arg_name='users')
@depends_on_tool(fetch_orders, arg_name='orders')
def generate_report(users: dict, orders: dict) -> dict:
    return {'report': 'Combined data from users and orders'}

Independent dependencies (fetch_users and fetch_orders) execute in parallel.

#DAG Patterns

Build complex dependency graphs (Directed Acyclic Graphs):

Python
# Level 1: No dependencies
@agent.toolify()
def step_a() -> dict:
    return {'step': 'A'}

@agent.toolify()
def step_b() -> dict:
    return {'step': 'B'}

# Level 2: Depends on Level 1
@agent.toolify()
@depends_on_tool(step_a, 'a_result')
def step_c(a_result: dict) -> dict:
    return {'step': 'C', 'from': a_result}

@agent.toolify()
@depends_on_tool(step_b, 'b_result')
def step_d(b_result: dict) -> dict:
    return {'step': 'D', 'from': b_result}

# Level 3: Depends on Level 2
@agent.toolify(final_tool=True)
@depends_on_tool(step_c, 'c_result')
@depends_on_tool(step_d, 'd_result')
class FinalReport(BaseModel):
    c_result: dict
    d_result: dict
    combined: str

Execution order:

  1. step_a and step_b run in parallel
  2. step_c and step_d run when their dependencies complete
  3. FinalReport runs last

#Execution-Control Dependencies

Execution-control decorators are metadata-only. They shape sequencing and reevaluation, but they do not create argument injection dependencies.

#Sequencing Without Data Flow

Use @depends_on_await_for when one tool must wait for another even though it does not consume the other tool's return value.

Python
from maivn import depends_on_await_for

@agent.toolify(description='Fetch latest records')
def fetch_records() -> dict:
    return {'records': [...]}

@agent.toolify(description='Write compliance audit entry after fetch')
@depends_on_await_for(fetch_records, timing='after', instance_control='all')
def write_audit_log() -> dict:
    return {'logged': True}

Use this for ordering, confirmation flows, audit trails, and any side-effecting tool that must wait even when no data is passed.

#Declaring Reevaluation Boundaries

Use @depends_on_reevaluate when planning must stop, inspect completed results, and resume in a later batch.

Python
from maivn import depends_on_reevaluate

@agent.toolify(description='Fetch document text')
def fetch_document() -> dict:
    return {'text': '...'}

@agent.toolify(description='Create final summary after reviewing fetched content')
@depends_on_reevaluate(fetch_document, timing='after', instance_control='all')
def summarize_document() -> dict:
    return {'summary': '...'}

This makes the runtime pause at the declared boundary so later work can use accumulated context instead of placeholder arguments.

#A Guaranteed Boundary

@depends_on_reevaluate is a hard runtime guarantee, not a hint: when you declare it, the runtime ensures the dependent does not start until the boundary's reevaluate cycle has completed with the target's actual result in scope. You do not have to rely on the model choosing to replan — declaring the boundary is sufficient, and the runtime de-duplicates if the model also happens to replan at the same point.

The current runtime projects reevaluate cycles as raw system_tool_chunk payloads with stage: "reevaluate" and phase: "reevaluate_accrued". The SDK normalizer currently drops these textless chunks; normalized enrichment listeners alone do not expose every cycle. The attribution fields distinguish:

  • source: "dependency" — the cycle honours a @depends_on_reevaluate boundary; it carries trigger_tool (the dependent) and target_tool (the target).
  • source: "llm" — the model chose to reevaluate on its own.

See Frontend Events for the full reevaluate event shape.

#Agent Dependencies

In multi-agent systems, tools can depend on other agents' outputs.

#Basic Pattern

Python
from maivn import Agent, Swarm, depends_on_agent

researcher = Agent(name='researcher', api_key='...')
writer = Agent(name='writer', api_key='...')

@researcher.toolify(description='Research a topic')
def research(topic: str) -> dict:
    return {'findings': f'Research on {topic}'}

@writer.toolify(description='Write based on research')
@depends_on_agent(researcher, arg_name='research_result')
def write_article(research_result: dict) -> dict:
    return {'article': f'Based on: {research_result}'}

swarm = Swarm(name='team', agents=[researcher, writer])

#Agent Dependency Resolution

When a tool depends on an agent:

  1. The dependent agent is invoked
  2. Its full output (from its tools) is collected
  3. That output is passed to the dependent tool

#Swarm Member Dependencies

Use swarm.member when the dependency belongs to the swarm's generated agent invocation tool, rather than to a tool defined inside an agent.

Python
from maivn import Agent, Swarm, depends_on_tool

swarm = Swarm(name='planning_team')

@swarm.toolify(description='Load launch context')
def load_context() -> dict:
    return {'segment': 'regional clinics'}

@depends_on_tool(load_context, arg_name='context')
def build_researcher() -> Agent:
    return Agent(name='researcher', api_key='...')

researcher = swarm.member(build_researcher)

editor = swarm.member.depends_on_agent(
    researcher,
    arg_name='research_notes',
)(Agent(name='editor', api_key='...', use_as_final_output=True))

Member dependencies are exposed as arguments on the generated agent invocation tool. Use this for swarm-level context, agent-to-agent handoffs, execution controls, and interrupt inputs that should be resolved before a member agent is invoked.

#Private Data Dependencies

Inject secrets into a tool argument without exposing them to the LLM.

#Basic Pattern

Python
from maivn import depends_on_private_data

@agent.toolify(description='Call external API')
@depends_on_private_data(data_key='api_key', arg_name='secret')
def call_api(query: str, secret: str) -> dict:
    # 'secret' is injected at execution time
    return {'result': f'API call with {query}'}

agent.private_data = {'api_key': 'sk-xxx-secret'}

#Multiple Secrets

Python
@agent.toolify()
@depends_on_private_data(data_key='db_password', arg_name='db_pass')
@depends_on_private_data(data_key='api_key', arg_name='api_key')
def connect_and_fetch(query: str, db_pass: str, api_key: str) -> dict:
    return {'data': 'fetched'}

agent.private_data = {
    'db_password': 'db-secret',
    'api_key': 'api-secret',
}

#Security Model

Private data follows strict security rules:

  1. Schema-only planning: the LLM sees only key names, never values
  2. Runtime injection: real values are injected into the tool argument at execution time
  3. Automatic redaction: tool results are scanned and known private values redacted before returning to the model
  4. SDK event boundary: resolved values are excluded from SDK listener and frontend-safe event payloads. Your tool code must still avoid logging or returning them.

See Private Data Guide for full details.

#Interrupt Dependencies

Collect user input during tool execution.

#Basic Pattern

Python
from maivn import depends_on_interrupt, default_terminal_interrupt

@agent.toolify(description='Greet user by name')
@depends_on_interrupt(
    arg_name='name',
    input_handler=default_terminal_interrupt,
    prompt='Please enter your name: ',
)
def greet(name: str) -> dict:
    return {'greeting': f'Hello, {name}!'}

#Custom Input Handler

When your handler has its own prompting logic, prompt can be omitted:

Python
def file_picker() -> str:
    # GUI dialog with its own UI - no prompt needed
    return show_file_dialog()

@agent.toolify()
@depends_on_interrupt(arg_name='file_path', input_handler=file_picker)
def process_file(file_path: str) -> dict:
    return {'processed': file_path}

With a prompt passed to the handler:

Python
def gui_input_handler(prompt: str) -> str:
    # Show a GUI dialog with the prompt
    return show_dialog(prompt)

@agent.toolify()
@depends_on_interrupt(
    arg_name='choice',
    input_handler=gui_input_handler,
    prompt='Select option (1-3): ',
)
def process_choice(choice: str) -> dict:
    return {'selected': choice}

#Interrupt Flow

  1. Tool execution reaches the interrupt
  2. Execution pauses
  3. input_handler is called with the prompt
  4. User provides input
  5. Execution resumes with the value

#Combining Dependencies

Mix dependency types as needed:

Python
@agent.toolify(description='Process with all inputs')
@depends_on_tool(fetch_data, arg_name='data')
@depends_on_private_data(data_key='api_key', arg_name='key')
@depends_on_interrupt(
    arg_name='confirm',
    input_handler=default_terminal_interrupt,
    prompt='Proceed? (y/n): ',
)
def full_process(data: dict, key: str, confirm: str) -> dict:
    if confirm.lower() != 'y':
        return {'status': 'cancelled'}
    return {'status': 'processed', 'records': len(data['records'])}

#Decorator Order

Decorators can be stacked in any order:

Python
# Order 1: toolify first
@agent.toolify()
@depends_on_tool(other, 'result')
def my_tool(result: dict) -> dict: ...

# Order 2: toolify last
@depends_on_tool(other, 'result')
@agent.toolify()
def my_tool(result: dict) -> dict: ...

Both work correctly. The SDK handles registration regardless of order.

#Validation

#Argument Validation

Dependency decorators record argument names without a general import-time signature check. Ensure each arg_name matches a function parameter or Pydantic field, inspect compile_tools(), and test the dependency path. A successful import does not prove that the declared injection can execute.

#Circular Dependencies

Circular dependencies are detected and prevented:

Plain text
@agent.toolify()
@depends_on_tool(tool_b, 'b')
def tool_a(b: dict) -> dict: ...

@agent.toolify()
@depends_on_tool(tool_a, 'a')  # Creates cycle!
def tool_b(a: dict) -> dict: ...

#Execution Behavior

#Parallel Execution

Independent tools run in parallel:

Python
@agent.toolify()
def tool_a() -> dict: ...  # Takes 2 seconds

@agent.toolify()
def tool_b() -> dict: ...  # Takes 3 seconds

@agent.toolify()
@depends_on_tool(tool_a, 'a')
@depends_on_tool(tool_b, 'b')
def final(a: dict, b: dict) -> dict: ...

# These tools can run concurrently; planning and runtime overhead also apply.

#Sequential When Required

Dependencies enforce ordering:

Python
@agent.toolify()
def step_1() -> dict: ...  # Takes 2 seconds

@agent.toolify()
@depends_on_tool(step_1, 'result')
def step_2(result: dict) -> dict: ...  # Takes 2 seconds

# step_2 waits for step_1; end-to-end latency also includes runtime overhead.

#Best Practices

#1. Use Clear Argument Names

Plain text
# Good
@depends_on_tool(fetch_users, arg_name='user_data')

# Less clear
@depends_on_tool(fetch_users, arg_name='x')

#2. Document Data Flow

Add comments for complex dependency graphs:

Python
# Data flow:
# fetch_metrics -> calculate_stats
#              \-> generate_charts
# Both feed into -> final_report

@agent.toolify()
@depends_on_tool(calculate_stats, 'stats')
@depends_on_tool(generate_charts, 'charts')
def final_report(stats: dict, charts: dict) -> dict: ...

#Next steps