Decorators
Dependency decorators for tools, agents, data, and interrupts.
#Decorators
Dependency decorators declare data flow between tools, agents, and external data sources. They enable the SDK and planner to resolve dependencies during execution.
#Import
from maivn import (
depends_on_tool,
depends_on_agent,
depends_on_private_data,
depends_on_interrupt,
depends_on_await_for,
depends_on_reevaluate,
requires_producer_reference,
tool_output,
# Toolset class pattern
toolify,
toolset,
# Permission helpers used with @toolify(permissions=...)
PermissionFlag,
PermissionSet,
require_permissions,
# Provider metadata for toolset classes
AuthMode,
ProviderCapability,
ProviderMetadata,
)#requires_producer_reference
Declares that one argument path accepts only a reference produced by a named upstream tool. Generator toolsets use this metadata to expose their dependency graph to the planner while enforcing the same requirement when the tool runs.
@requires_producer_reference(
'block.content',
producer_tool_name='DOCUMENTS_compose_artifact',
)
@agent.toolify(description='Insert a composed document block')
def put_block(block: dict) -> dict:
...This metadata does not inject a whole prior result. The receiving tool must still validate that the nominal reference exists and belongs to its own workspace.
#tool_output
Declare the output schema for a function or method tool when the Python return type is too generic to describe the public result contract.
def tool_output(
schema: type[BaseModel] | dict[str, object],
) -> Callable#Parameters
#Example
from pydantic import BaseModel
from maivn import Agent, tool_output
class SearchMessagesResult(BaseModel):
message_ids: list[str]
next_page_token: str | None = None
agent = Agent(name='mail', api_key='...')
@tool_output(SearchMessagesResult)
def search_messages(query: str) -> dict[str, object]:
"""Search recent messages."""
return {'message_ids': ['m-1'], 'next_page_token': None}
agent.add_tool(search_messages)@tool_output(...) is not a registration marker. It works with direct agent.add_tool(fn) registration, and with toolset methods that are already discovered through @toolify or @toolset(require_marker=False). It does not expose an otherwise unmarked toolset method.
For app-specific result handling, declare @tool_output on a wrapper function. Current add_tool and add_toolset methods do not accept override arguments.
#depends_on_tool
Declare a dependency on another tool's output.
def depends_on_tool(
tool_ref: str | BaseTool | Callable,
arg_name: str,
*,
revision_policy: Literal['on_validation_error'] | None = None,
result_scope: Literal['invocation', 'conversation'] = 'invocation',
) -> Callable#Parameters
#Example
from maivn import Agent, depends_on_tool
agent = Agent(name='data_agent', api_key='...')
@agent.toolify(description='Fetch raw data')
def fetch_data(source: str) -> dict:
return {'source': source, 'records': [...]}
@agent.toolify(description='Process fetched data')
@depends_on_tool(fetch_data, arg_name='raw_data')
def process_data(raw_data: dict) -> dict:
# 'raw_data' is automatically populated with fetch_data's output
return {'processed': len(raw_data['records'])}result_scope='invocation' is the safe default. The producer must complete in the current invocation before the consumer can run. Use result_scope='conversation' only for a stable result that may remain valid on a later turn in the same thread:
@agent.toolify(description='Load the customer profile')
def load_customer_profile(customer_id: str) -> dict:
return {'customer_id': customer_id, 'service_tier': 'enterprise'}
@agent.toolify(description='Draft a reply using the customer profile')
@depends_on_tool(
load_customer_profile,
arg_name='profile',
result_scope='conversation',
)
def draft_reply(profile: dict) -> dict:
return {'reply': f'Reply for {profile["customer_id"]}'}Conversation scope is permission on this dependency edge, rather than a request to restore the whole graph. Reuse requires the same thread and matching producer spec, and the platform supplies only a trusted, successfully stored result. If the producer runs again, that fresh attempt is authoritative: its result wins, and a failed refresh does not fall back to an older success. Keep confirmation, authorization, and freshness-sensitive dependencies at invocation scope.
#Chaining Multiple Dependencies
@agent.toolify(description='Step 1')
def step_one() -> dict:
return {'step': 1}
@agent.toolify(description='Step 2')
def step_two() -> dict:
return {'step': 2}
@agent.toolify(description='Final step')
@depends_on_tool(step_one, arg_name='result_one')
@depends_on_tool(step_two, arg_name='result_two')
def final_step(result_one: dict, result_two: dict) -> dict:
return {'combined': [result_one, result_two]}#depends_on_agent
Declare a dependency on another agent's output.
def depends_on_agent(
agent_ref: str | Any,
arg_name: str,
) -> Callable#Parameters
#Example
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'Article based on: {research_result}'}
swarm = Swarm(name='team', agents=[researcher, writer])#How It Works
When a tool depends on an agent:
- The swarm identifies the dependency
- The dependent agent is invoked first
- Its output is passed to the tool's specified argument
#depends_on_private_data
Inject a private value into a tool at execution time.
def depends_on_private_data(
data_key: str,
arg_name: str,
) -> Callable#Parameters
#Example
from maivn import Agent, depends_on_private_data
agent = Agent(name='api_agent', api_key='...')
@agent.toolify(description='Call external API')
@depends_on_private_data(data_key='api_secret', arg_name='secret')
def call_external_api(query: str, secret: str) -> dict:
# 'secret' is injected at runtime, never exposed to LLM
return {'result': f'API response for {query}'}
# Set private data
agent.private_data = {'api_secret': 'sk-xxx-secret-key'}#Security Model
Private data follows strict security boundaries:
- Schema-only planning: LLM sees only field names/types, never values
- Runtime injection: Values injected at execution time, held within the runtime
- Automatic redaction: Results containing private data are redacted
- SDK event boundary: Resolved private values are excluded from SDK listener and frontend-safe event payloads. Your tool code remains responsible for not logging or returning them.
#Pydantic Class Targets
@depends_on_private_data works on @agent.toolify'd Pydantic model classes the same way it works on function tools. The declared field is hidden from the model-facing input schema and populated at SDK validation time:
from maivn import Agent, depends_on_private_data
from pydantic import BaseModel
agent = Agent(name='audit_agent', api_key='...')
@depends_on_private_data(data_key='customer_ssn', arg_name='ssn')
@agent.toolify(name='audit_record', description='Record an audit entry', final_tool=True)
class AuditRecord(BaseModel):
ssn: str # injected at execution time from private_data['customer_ssn']
note: str # supplied by the LLM
def __call__(self) -> dict:
return {'ssn_last4': self.ssn[-4:], 'note': self.note}The decorator does not validate a missing model field at import time. A bad name can fail later during schema generation or validation. Match the name to an actual field and inspect the compiled tool before serving it.
#depends_on_interrupt
Collect user input during tool execution.
def depends_on_interrupt(
arg_name: str,
input_handler: Callable[[str], Any],
prompt: str = "",
input_type: InputType | None = None,
choices: list[str] | None = None,
) -> CallableInputType is Literal['text', 'choice', 'boolean', 'number', 'email', 'password'].
#Parameters
#Example
from maivn import Agent, depends_on_interrupt, default_terminal_interrupt
agent = Agent(name='interactive', api_key='...')
@agent.toolify(description='Greet the user')
@depends_on_interrupt(
arg_name='user_name',
input_handler=default_terminal_interrupt,
prompt='Please enter your name: ',
)
def greet_user(user_name: str) -> dict:
return {'greeting': f'Hello, {user_name}!'}#Custom Input Handler
When your handler has its own prompting logic, prompt can be omitted:
def file_picker_dialog() -> str:
# Custom GUI dialog - no prompt needed
return show_file_picker()
@agent.toolify()
@depends_on_interrupt(arg_name='file_path', input_handler=file_picker_dialog)
def process_file(file_path: str) -> dict:
return {'processed': file_path}With a prompt:
def custom_handler(prompt: str) -> str:
# Custom logic (GUI dialog, API call, etc.)
return get_input_from_somewhere(prompt)
@agent.toolify()
@depends_on_interrupt(
arg_name='choice',
input_handler=custom_handler,
prompt='Select an option (1-3): ',
)
def process_choice(choice: str) -> dict:
return {'selected': choice}#Interrupt Flow
- Tool execution reaches the interrupt dependency
- Execution pauses
input_handleris called with the prompt- User provides input
- Execution resumes with the input value
#depends_on_await_for
Declare a sequencing dependency without injecting another tool's output into an argument.
def depends_on_await_for(
tool_ref: str | BaseTool | Callable,
*,
timing: Literal['before', 'after'] = 'after',
instance_control: Literal['each', 'all'] = 'each',
) -> Callabletimingandinstance_controlare keyword-only.
#Parameters
#Example
from maivn import Agent, depends_on_await_for
agent = Agent(name='workflow_agent', api_key='...')
@agent.toolify(description='Fetch raw data')
def fetch_data(source: str) -> dict:
return {'source': source}
@agent.toolify(description='Write audit log after fetch completes')
@depends_on_await_for(fetch_data)
def write_audit_entry() -> dict:
return {'logged': True}@depends_on_await_for is metadata-only. It affects execution order, but it does not inject a tool result into a function argument.
#depends_on_reevaluate
Declare that planning must pause at a specific boundary and insert reevaluate.
def depends_on_reevaluate(
tool_ref: str | BaseTool | Callable,
*,
timing: Literal['before', 'after'] = 'after',
instance_control: Literal['each', 'all'] = 'each',
) -> Callabletimingandinstance_controlare keyword-only.
#Parameters
#Example
from maivn import Agent, depends_on_reevaluate
agent = Agent(name='planning_agent', api_key='...')
@agent.toolify(description='Fetch source material')
def fetch_source() -> dict:
return {'content': '...'}
@agent.toolify(description='Summarize after reviewing fetched content')
@depends_on_reevaluate(fetch_source, timing='after', instance_control='all')
def summarize_source() -> dict:
return {'summary': '...'}@depends_on_reevaluate is metadata-only on the SDK side, but the runtime enforces it: when the dependent tool is registered, the runtime ensures a reevaluate cycle runs at the declared boundary unless the model already planned a satisfying one, and the dependent tool is gated so it cannot run before that reevaluate completes. You are not required to make the model plan reevaluate itself for declared boundaries. The runtime guarantees the boundary either way, and a model-planned reevaluate at the same point is reconciled to avoid duplication.
When a dependency-driven reevaluate fires, an enrichment event (phase="reevaluate_accrued") is emitted with source="dependency", plus trigger_tool and target_tool attribution. Model-planned reevaluates emit the same event with source="llm". See Reevaluate cycles for the payload shape.
A boundary that was already satisfied earlier in the same session is tracked by the runtime so future re-plans of the same chain don't cascade additional reevaluate cycles.
#Decorator Order
Decorators can be stacked in any order. The @agent.toolify() decorator should typically be closest to the function:
@depends_on_tool(step_one, 'result_one')
@depends_on_private_data('secret', 'api_key')
@agent.toolify(description='Final processing')
def process(result_one: dict, api_key: str) -> dict:
return {'processed': True}Or with @agent.toolify() on top:
@agent.toolify(description='Final processing')
@depends_on_tool(step_one, 'result_one')
@depends_on_private_data('secret', 'api_key')
def process(result_one: dict, api_key: str) -> dict:
return {'processed': True}Both orders work correctly.
The same dependency decorators also work before registering tools with Agent(..., tools=[...]) or agent.add_tool(...):
@depends_on_tool(step_one, 'result_one')
def process(result_one: dict) -> dict:
"""Process the first step result."""
return {'processed': True}
agent = Agent(name='processor', api_key='...', tools=[step_one, process])For swarm member agents, stack dependency decorators before @swarm.member on a zero-argument agent factory, or use the builder form such as swarm.member.depends_on_agent(...):
@swarm.member
@depends_on_tool(load_context, 'context')
def researcher() -> Agent:
return Agent(name='researcher', api_key='...')
writer = swarm.member.depends_on_agent(
researcher,
arg_name='research_notes',
)(Agent(name='writer', api_key='...'))#Validation
Dependency decorators record metadata. Do not rely on them to reject every unknown arg_name at import time. Match each argument to a real function parameter or model field; compile the tools and test the dependency path before deployment. compose_argument_policy has its own stricter target validation.
#Dependency Resolution
The SDK builds a dependency graph and resolves dependencies in order:
- Tools with no dependencies execute first (or in parallel)
- Dependent tools wait for their dependencies
- The final tool executes last (if
force_final_tool=True)
#Parallel Execution
Independent dependencies execute in parallel:
@agent.toolify()
def fetch_a() -> dict: ...
@agent.toolify()
def fetch_b() -> dict: ...
@agent.toolify()
@depends_on_tool(fetch_a, 'a')
@depends_on_tool(fetch_b, 'b')
def combine(a: dict, b: dict) -> dict:
# fetch_a and fetch_b run in parallel
return {'combined': [a, b]}#Toolset decorators
The dependency decorators above declare data flow between individual tools. The toolset decorators below promote a configured class to a collection of related tools that share an instance (a database handle, an HTTP client, an OAuth token, etc.).
#@toolset
Marks a class as a toolset. Two forms:
from maivn import toolset
@toolset
class Plain:
...
@toolset(prefix='github', tags=['developer'], require_marker=True)
class Configured:
...#@toolify (method form)
When applied inside a @toolset class, @toolify marks an individual method as a tool and declares its metadata. The same @toolify symbol that decorates free functions is used here; the registration path is selected automatically by the SDK.
from maivn import PermissionFlag, PermissionSet, toolify
class Example:
@toolify
def simple(self) -> None:
"""Bare form. No permission tags, no destructive marker."""
@toolify(
name='renamed', # override the exposed tool name
description='...', # override the docstring
permissions=PermissionSet(PermissionFlag.WRITE),
destructive=False,
always_execute=False,
final_tool=False,
tags=['payments'],
metadata={'audit_zone': 'high'},
before_execute=...,
after_execute=...,
)
def write_payment(self, ...): ...@toolify coexists with the dependency decorators above (@depends_on_tool, @depends_on_agent, @depends_on_reevaluate, etc.). Order does not matter. Each decorator attaches its own attribute and the SDK's dependency collector reads them all at registration time.
#Permission helpers
PermissionFlag is a Flag enum; PermissionSet is an immutable bitfield wrapper. The SDK auto-derives tag names from the declared permissions (READ → "read", DELETE → "delete" + "destructive", etc.) so filters like include_tags=["read"] and exclude_tags=["destructive"] work without you having to add those strings to each method's tags= list.
Setting destructive=True on @toolify(...) also adds "destructive" regardless of the permission flags.
Hosts can validate a caller's grant against a tool's declared set with require_permissions(granted, required), which raises PermissionError if any flag in required is missing.
#ProviderMetadata
Attach a ProviderMetadata instance as a class attribute named metadata to advertise provider identity, supported auth modes, scopes, and capabilities. The runtime treats the attribute as descriptive only and never inspects it for behavior, but hosts can surface it in catalogs or audit logs.
from maivn import AuthMode, ProviderCapability, ProviderMetadata, toolset
@toolset(prefix='reports')
class WeeklyReportsToolSet:
metadata = ProviderMetadata(
name='weekly_reports',
display_name='Weekly KPI Reports',
version='1.0.0',
description='Pull weekly KPI reports from an internal warehouse.',
auth_modes=(AuthMode.NONE,),
capabilities=frozenset({ProviderCapability.READ}),
tags=('analytics',),
)
...#Registration
agent.add_toolset(instance)
agent.add_toolset(other_instance) # multiple toolsets accumulateadd_toolset walks the class, finds every @toolify-marked method, and creates one ToolMetadata entry per method bound to the instance. Tools are appended to agent.tools so anything that inspects the agent's tool list sees them the same way it sees free callables.
#Filter kwargs
agent.add_toolset(instance, *,
include=None, # list[str]: only these method names register
exclude=None, # list[str]: skip these method names
include_tags=None, # list[str]: only methods with any matching tag
exclude_tags=None, # list[str]: skip methods with any matching tag
)Filters compose: a method must pass every active filter. include / exclude match the unprefixed method name (or the name= override on @toolify). Tag matching uses the union of user-declared tags and auto-derived permission tags described above.
If filters drop every method and the toolset has require_marker=True (the default), add_toolset raises ValueError.
#App-specific changes
Current add_toolset does not accept overrides= and add_tool does not accept override=. Use the supported registration arguments or an application wrapper. The exported ToolOverride model is retained for compatibility; accepting a model is not proof that the current compilation path applies it.
#See Also
- Agent -
@agent.toolify()decorator - Tools Guide - Registration styles including the toolset class pattern
- Dependencies Guide - Detailed patterns
- Private Data Guide - Security model