Skip to content

Agents

Understanding the Agent abstraction -- the core building block of Flux.


Overview

The Agent is the central abstraction in Flux. It represents an AI entity that can follow instructions, use tools, hand off to other agents, and enforce guardrails -- all configured through a single, immutable dataclass.

Every interaction in Flux begins with an Agent. Whether you are building a simple question-answering bot or a complex multi-agent system, you define your behavior through one or more Agent instances.


The Agent Dataclass

Agent is a frozen (immutable) dataclass defined in flux/agent.py:

from dataclasses import dataclass, field, replace
from flux.agent import Agent, AgentSettings

@dataclass(frozen=True)
class Agent:
    name: str
    instructions: str | Callable[..., str] = ""
    model: str | Model | None = None
    tools: tuple[Tool, ...] = ()
    handoffs: tuple[Handoff | Agent, ...] = ()
    guardrails: tuple[InputGuardrail | OutputGuardrail, ...] = ()
    output_type: type | None = None
    settings: AgentSettings = field(default_factory=AgentSettings)

Every field is defined at construction time. Once created, an Agent cannot be mutated -- you use clone() to create modified copies.


Creating Agents

Minimal Agent

The simplest agent requires only a name:

from flux.agent import Agent

agent = Agent(name="greeter")

Agent with Instructions

Pass a static instruction string to guide the agent's behavior:

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant. Be concise and accurate.",
)

Fully Configured Agent

Combine tools, model selection, handoffs, guardrails, and settings:

from flux.agent import Agent, AgentSettings
from flux.tools import tool
from flux.models import ModelSettings

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"Sunny, 22C in {city}"

agent = Agent(
    name="weather_bot",
    instructions="You help users check the weather.",
    model="ollama/llama3.2",
    tools=(get_weather,),
    settings=AgentSettings(
        max_turns=5,
        model_settings=ModelSettings(temperature=0.7),
    ),
)

Static vs Dynamic Instructions

Agent instructions can be either a static string or a callable that receives a RunContext and returns a string dynamically.

Static Instructions

agent = Agent(
    name="analyst",
    instructions="You are a data analyst. Always respond with JSON.",
)

Dynamic Instructions (Callable)

Use a callable when instructions depend on runtime context -- for example, user identity, session state, or external configuration:

from flux.context import RunContext

def dynamic_instructions(ctx: RunContext | None) -> str:
    user_name = ctx.user_context if ctx else "Guest"
    return f"You are a helpful assistant for {user_name}. Be friendly and precise."

agent = Agent(
    name="personal_assistant",
    instructions=dynamic_instructions,
)

The get_instructions() method resolves both forms transparently:

# Resolves static string or calls the callable
instructions = agent.get_instructions(context=run_context)

Model Assignment

The model field accepts three forms:

Type Example Description
str "ollama/llama3.2" Resolved via the ModelRegistry prefix matching
Model OpenAIModel(model="gpt-4o-mini") A provider instance directly
None None (default) Falls back to the configuration default model
from flux.models import OllamaModel, OpenAIModel, AnthropicModel

# String-based resolution
agent_a = Agent(name="a", model="ollama/llama3.2")

# Direct provider instance
agent_b = Agent(name="b", model=OpenAIModel(model="gpt-4o-mini"))

# Explicit provider
agent_c = Agent(name="c", model=AnthropicModel(model="claude-sonnet-4-20250514"))

# Use config default
agent_d = Agent(name="d", model=None)

Tools, Handoffs, and Guardrails

All three are stored as tuples, making them immutable and hashable:

agent = Agent(
    name="supervisor",
    tools=(tool_a, tool_b),             # Tools this agent can invoke
    handoffs=(agent_a, handoff_to_b),   # Agents or Handoff objects to transfer to
    guardrails=(input_guardrail,),      # Input and output guardrails
)

Because tuples are immutable, you cannot append directly. Use clone() to add or remove items:

agent_with_tool = agent.clone(tools=agent.tools + (new_tool,))
agent_without_handoff = agent.clone(handoffs=agent.handoffs[:1])

Immutability and clone()

Agent is frozen, meaning attribute assignment raises FrozenInstanceError:

agent = Agent(name="test")
agent.name = "other"  # raises dataclasses.FrozenInstanceError

Instead, use clone() to produce a modified copy:

# Create a variant with different instructions
variant = agent.clone(
    name="test_v2",
    instructions="You are a stricter version of the assistant.",
)

# Add a tool to an existing agent
agent_with_tools = agent.clone(
    tools=agent.tools + (new_tool,),
)

# Change model settings
agent低成本 = agent.clone(
    settings=AgentSettings(
        max_turns=3,
        model_settings=ModelSettings(temperature=0.0),
    ),
)

Under the hood, clone() uses dataclasses.replace():

def clone(self, **kwargs: Any) -> Agent:
    return replace(self, **kwargs)

Agent Settings

The AgentSettings dataclass controls agent-level runtime behavior:

@dataclass
class AgentSettings:
    max_turns: int = 10
    model_settings: ModelSettings = field(default_factory=ModelSettings)

max_turns

Limits the number of agent turns (model call + tool execution cycles) in a single run. This prevents infinite loops when an agent repeatedly calls tools.

settings = AgentSettings(max_turns=20)  # Allow up to 20 turns

model_settings

A ModelSettings instance that controls generation parameters:

from flux.models import ModelSettings

settings = AgentSettings(
    max_turns=10,
    model_settings=ModelSettings(
        temperature=0.7,
        top_p=0.9,
        max_tokens=2048,
    ),
)

ModelSettings supports these fields:

Field Type Description
temperature float Sampling temperature
top_p float Nucleus sampling threshold
max_tokens int Maximum tokens in response
frequency_penalty float Penalize frequent tokens
presence_penalty float Penalize repeated tokens
stop list[str] Stop sequences
seed int Random seed for reproducibility
tool_choice str \| dict Force or disable tool use
parallel_tool_calls bool Allow parallel tool calls
extra dict Provider-specific overrides

Agent Lifecycle

The following diagram shows how an Agent moves through a Flux run:

flowchart TD
    A["Agent Created\n(frozen dataclass)"] --> B["RunContext Prepared"]
    B --> C{"Model Called\n(complete or stream)"}
    C -->|"Text Response"| D["Response Returned"]
    C -->|"Tool Calls"| E["Tools Executed\n(ToolContext)"]
    E -->|"max_turns not reached"| C
    E -->|"max_turns reached"| D
    C -->|"Handoff"| F["Target Agent Activated"]
    F --> C
    C -->|"Guardrail Triggered"| G["Guardrail Action\n(block / modify / escalate)"]
    G -->|"Allow"| C
    G -->|"Block"| D

Handoffs Between Agents

Agents can hand off control to other agents, enabling multi-agent orchestration:

from flux.handoffs import Handoff

triage = Agent(
    name="triage",
    instructions="Route the user to the right specialist.",
    handoffs=(billing_agent, technical_agent),
)

billing_agent = Agent(
    name="billing",
    instructions="You handle billing inquiries only.",
)

technical_agent = Agent(
    name="technical",
    instructions="You handle technical support only.",
)

When the triage agent decides the user needs billing help, it hands off to billing_agent, which takes over the conversation with its own instructions and tools.


Structured Output

Use output_type to request structured responses from the agent:

from dataclasses import dataclass

@dataclass
class WeatherReport:
    city: str
    temperature: int
    condition: str

agent = Agent(
    name="weather_reporter",
    instructions="Generate weather reports as structured data.",
    output_type=WeatherReport,
)

When output_type is set, the runner validates the model's response against the provided schema.


Best Practices

Keep agents focused

Give each agent a single responsibility. An agent that tries to do everything will have diluted instructions and poor performance. Use handoffs to compose specialized agents.

Use dynamic instructions for personalization

When the same agent serves different users or contexts, use a callable for instructions to inject context-specific behavior.

Set max_turns defensively

Always consider setting max_turns on agents that use tools. A low limit (5-10) prevents runaway loops; a higher limit (15-20) is suitable for complex multi-step tasks.

Prefer clone() over recreation

When you need a variant of an existing agent, use clone() rather than constructing a new Agent from scratch. This ensures you only change the fields you intend to.

Use tuples, not lists

Tools, handoffs, and guardrails are tuples by design. Build them incrementally with concatenation:

tools = (tool_a,)
tools = tools + (tool_b,)  # Not tools.append()

Frozen means frozen

Agent instances cannot be modified after creation. If you find yourself wanting to mutate an agent, you likely need clone() instead.


See Also

  • Tools -- Extending agent capabilities with function calling
  • Providers -- LLM provider architecture and model configuration
  • Sessions -- Conversation persistence for multi-turn interactions
  • Handoffs -- Multi-agent orchestration patterns
  • Guardrails -- Input and output validation