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:
It also supports two execution controls:
#Tool Dependencies
Chain tools by injecting one tool's output into another.
#Basic Pattern
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(...):
@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:
@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
@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):
# 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: strExecution order:
step_aandstep_brun in parallelstep_candstep_drun when their dependencies completeFinalReportruns 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.
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.
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_reevaluateboundary; it carriestrigger_tool(the dependent) andtarget_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
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:
- The dependent agent is invoked
- Its full output (from its tools) is collected
- 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.
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
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
@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:
- Schema-only planning: the LLM sees only key names, never values
- Runtime injection: real values are injected into the tool argument at execution time
- Automatic redaction: tool results are scanned and known private values redacted before returning to the model
- 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
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:
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:
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
- Tool execution reaches the interrupt
- Execution pauses
input_handleris called with the prompt- User provides input
- Execution resumes with the value
#Combining Dependencies
Mix dependency types as needed:
@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:
# 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:
@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:
@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:
@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
# 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:
# 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
- Decorators Reference - API details
- Private Data Guide - Security model
- Multi-Agent Guide - Agent dependencies