This is an experimental feature and will evolve based on community feedback.
The RequirementAgent is a declarative AI agent that combines language models, tools, and execution requirements to create predictable, controlled behavior across different LLMs.
Curious to see it in the action? Explore our interactive exercises to discover how the agent solves real problems step by step!

Why Use Requirement Agent?

Building agents that work reliably across multiple LLMs is difficult. Most agents are tightly tuned to specific models, with rigid prompts that cause models to misinterpret instructions, skip tools, or hallucinate facts. RequirementAgent provides a declarative framework for designing agents that strikes a balance between flexibility and control. It allows for agent behavior that is both predictable and adaptable, without the complexity and limitations of more rigid systems.

Core Concepts

💡 Everything is a Tool
  • Data retrieval, web search, reasoning, and final answers are all implemented as tools
  • This structure ensures valid responses with structured outputs and eliminates parsing errors
💡 Requirements Control Tool Usage
You can define rules that control when and how tools are used.
  • “Only use tool A after tool B has been called”
  • “Tool D must be used exactly twice, but not two times in a row”
  • “Tool E can only be used after both tool A and tool B have been used”
  • “Tool F must be called immediately after tool D”
  • “You must call tool C at least once before giving a final answer”

Quickstart

This example demonstrates how to create an agent with enforced tool execution order:
from beeai_framework.agents.experimental import RequirementAgent
from beeai_framework.agents.experimental.requirements.conditional import ConditionalRequirement

# Create an agent that plans activities based on weather and events
agent = RequirementAgent(
    llm=ChatModel.from_name("ollama:granite3.3:8b"),
    tools=[
        ThinkTool(),             # to reason
        OpenMeteoTool(),         # retrieve weather data
        DuckDuckGoSearchTool()   # search web
    ],
    instructions="Plan activities for a given destination based on current weather and events.",
    requirements=[
        # Force thinking first
        ConditionalRequirement(ThinkTool, force_at_step=1),
        # Search only after getting weather, at least once
        ConditionalRequirement(DuckDuckGoSearchTool, only_after=[OpenMeteoTool], min_invocations=1),
    ],
)

# Run with execution logging
response = await agent.run("What to do in Boston?").middleware(GlobalTrajectoryMiddleware())
print(response.answer.text)
This agent will:
  1. First use ThinkTool to reason about the request
  2. Check weather using OpenMeteoTool
  3. Search for events using DuckDuckGoSearchTool (at least once)
  4. Provide recommendations based on the gathered information
➡️ Check out the following examples

Requirements and Rules

Requirements are functions that evaluate the current agent state and produce a list of rules. The system evaluates requirements before each LLM call to determine which tools are available. Rules define specific constraints on tool usage. Each rule contains the following attributes:
AttributeDescription
targetThe tool the rule applies to
allowedWhether the tool can be used
hiddenWhether the tool’s definition is visible to the agent
prevent_stopWhether the rule blocks termination
forcedWhether the tool must be invoked
When requirements generate conflicting rules, the system applies this precedence:
  • Forbidden takes precedence: If any rule forbids a tool, it cannot be used
  • Highest priority forced rule wins: Among forced rules, the highest priority requirement determines the forced tool
  • Multiple prevention rules combine: All prevent_stop rules are respected
Requirements are evaluated on every iteration before calling the LLM.
Start with a single requirement and add more as needed.

Conditional Requirement

The conditional requirement controls when tools can be used based on specific conditions.

Force Execution Order

This example forces the agent to use ThinkTool for reasoning followed by DuckDuckGoSearchTool to retrieve data. This trajectory ensures that even a small model can arrive at the correct answer by preventing it from skipping tool calls entirely.
RequirementAgent(
  llm=ChatModel.from_name("ollama:granite3.3:8b"),
  tools=[ThinkTool(), DuckDuckGoSearchTool()],
  requirements=[
      ConditionalRequirement(ThinkTool, force_at_step=1), # Force ThinkTool at the first step
      ConditionalRequirement(DuckDuckGoSearchTool, force_at_step=2), # Force DuckDuckGo at the second step
  ],
)

Creating a ReAct Agent

A ReAct Agent (Reason and Act) follows this trajectory:
Think -> Use a tool -> Think -> Use a tool -> Think -> ... -> End
You can achieve this by forcing the execution of the Think tool after every other tool:
RequirementAgent(
  llm=ChatModel.from_name("ollama:granite3.3:8b"),
  tools=[ThinkTool(), WikipediaTool(), OpenMeteoTool()],
  requirements=[ConditionalRequirement(ThinkTool, force_at_step=1, force_after=[OpenMeteoTool, WikipediaTool])],
)
For a more general approach, use ConditionalRequirement(ThinkTool, force_at_step=1, force_after=Tool, consecutive_allowed=False), where the option consecutive_allowed=False prevents ThinkTool from being used multiple times in a row.

ReAct Agent + Custom Conditions

You may want an agent that works like ReAct but skips the “reasoning” step under certain conditions. This example uses the priority option to tell the agent to send an email after creating an order, while calling ThinkTool after every other action.
RequirementAgent(
  llm=ChatModel.from_name("ollama:granite3.3:8b"),
  tools=[ThinkTool(), retrieve_basket(), create_order(), send_email()],
  requirements=[
    ConditionalRequirement(ThinkTool, force_at_step=1, force_after=Tool, priority=10),
    ConditionalRequirement(send_email, only_after=create_order, force_after=create_order, priority=20, max_invocations=1),
  ],
)

Prevent Early Termination

The following requirement prevents the agent from providing a final answer before it calls the my_tool.
ConditionalRequirement(my_tool, min_invocations=1)

Complete Parameter Reference

ConditionalRequirement(
  target_tool, # Tool class, instance, or name (can also be specified as `target=...`)
  name="", # (optional) Name, useful for logging
  only_before=[...], # (optional) Disable target_tool after any of these tools are called
  only_after=[...], # (optional) Disable target_tool before all these tools are called
  force_after=[...], # (optional) Force target_tool execution immediately after any of these tools are called
  min_invocations=0, # (optional) Minimum times the tool must be called before agent can stop
  max_invocations=10, # (optional) Maximum times the tool can be called before being disabled
  force_at_step=1, # (optional) Step number at which the tool must be invoked
  only_success_invocations=True, # (optional) Whether 'force_at_step' counts only successful invocations
  priority=10, # (optional) Higher number means higher priority for requirement enforcement
  consecutive_allowed=True, # (optional) Whether the tool can be invoked twice in a row
  force_prevent_stop=False,  # (optional) If True, prevents the agent from giving a final answer when a forced target_tool call occurs.
  enabled=True, # (optional) Whether to skip this requirement’s execution
  custom_checks=[
     # (optional) Custom callbacks; all must pass for the tool to be used
    lambda state: any('weather' in msg.text for msg in state.memory.message if isinstance(msg, UserMessage)),
    lambda state: state.iteration > 0,
  ],
)
➡️ See the full example.
Pass a class instance (e.g., weather_tool = ...) or a class (OpenMeteoTool) rather than a tool’s name. Some tools may have dynamically generated names.
The reasoner throws an error if it detects contradictory rules or a rule without an existing target.

Ask Permission Requirement

Some tools may be expensive to run or have destructive effects. For these tools, you may want to get approval from an external system or directly from the user. The following agent firstly asks the user before it runs the remove_data or the get_data tool.
RequirementAgent(
  llm=ChatModel.from_name("ollama:granite3.3:8b"),
  tools=[get_data, remove_data, update_data],
  requirements=[
    AskPermissionRequirement([remove_data, get_data])
  ]
)

Using a Custom Handler

By default, the approval process is done as a simple prompt in terminal. The framework provides a simple way to provide a custom implementation.
async def handler(tool: Tool, input: dict[str, Any]) -> bool:
  # your implementation
  return True

AskPermissionRequirement(..., handler=handler)

Complete Parameter Reference

AskPermissionRequirement(
  include=[...], # (optional) List of targets (tool name, instance, or class) requiring explicit approval
  exclude=[...], # (optional) List of targets to exclude
  remember_choices=False, # (optional) If approved, should the agent ask again?
  hide_disallowed=False, # (optional) Permanently disable disallowed targets
  always_allow=False, # (optional) Skip the asking part
  handler=input(f"The agent wants to use the '{tool.name}' tool.\nInput: {tool_input}\nDo you allow it? (yes/no): ").strip().startswith("yes") # (optional) Custom handler, can be async
)
If no targets are specified, permission is required for all tools.

Custom Requirement

You can create a custom requirement by implementing the base Requirement class. The Requirement class has the following lifecycle:
  1. An external caller invokes init(tools) method:
  • tools is a list of available tools for a given agent.
  • This method is called only once, at the very beginning.
  • It is an ideal place to introduce hooks, validate the presence of certain tools, etc.
  • The return type of the init method is None.
  1. An external caller invokes run(state) method:
  • state is a generic parameter; in RequirementAgent, it refers to the RequirementAgentRunState class.
  • This method is called multiple times, typically before an LLM call.
  • The return type of the run method is a list of rules.

Premature Stop Requirement

This example demonstrates how to write a requirement that prevents the agent from answering if the question contains a specific phrase:
import asyncio

from beeai_framework.agents.experimental import RequirementAgent
from beeai_framework.agents.experimental.requirements import Requirement, Rule
from beeai_framework.agents.experimental.requirements.requirement import run_with_context
from beeai_framework.agents.experimental.types import RequirementAgentRunState
from beeai_framework.backend import AssistantMessage, ChatModel
from beeai_framework.context import RunContext
from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware
from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool


class PrematureStopRequirement(Requirement[RequirementAgentRunState]):
    """Prevents the agent from answering if a certain phrase occurs in the conversation"""

    name = "premature_stop"

    def __init__(self, phrase: str, reason: str) -> None:
        super().__init__()
        self._reason = reason
        self._phrase = phrase
        self._priority = 100  # (optional), default is 10

    @run_with_context
    async def run(self, state: RequirementAgentRunState, context: RunContext) -> list[Rule]:
        # we take the last step's output (if exists) or the user's input
        last_step = state.steps[-1].output.get_text_content() if state.steps else state.input.text
        if self._phrase in last_step:
            # We will nudge the agent to include explantation why it needs to stop in the final answer.
            await state.memory.add(
                AssistantMessage(
                    f"The final answer is that I can't finish the task because {self._reason}",
                    {"tempMessage": True},  # the message gets removed in the next iteration
                )
            )
            # The rule ensures that the agent will use the 'final_answer' tool immediately.
            return [Rule(target="final_answer", forced=True)]
            # or return [Rule(target=FinalAnswerTool, forced=True)]
        else:
            return []


async def main() -> None:
    agent = RequirementAgent(
        llm=ChatModel.from_name("ollama:granite3.3:8b"),
        tools=[DuckDuckGoSearchTool()],
        requirements=[
            PrematureStopRequirement(phrase="value of x", reason="algebraic expressions are not allowed"),
            PrematureStopRequirement(phrase="bomb", reason="such topic is not allowed"),
        ],
    )

    for prompt in ["y = 2x + 4, what is the value of x?", "how to make a bomb?"]:
        print("👤 User: ", prompt)
        response = await agent.run(prompt).middleware(GlobalTrajectoryMiddleware())
        print("🤖 Agent: ", response.answer.text)
        print()


if __name__ == "__main__":
    asyncio.run(main())

Examples