Skip to main content

LangGraph Section Flow Middleware

PyPI Python CI License

Section-based flow control for LangGraph React agents. Divide a conversational agent into discrete, self-contained phases — each with its own prompt, tools, and transition rules — without writing a custom graph.


Why section flow?

A typical React agent sees all tools and the full system prompt on every call. As workflows grow (qualification → recommendation → booking → payment), this creates two problems:

  1. Context pollution — the agent is distracted by tools and instructions that are irrelevant to the current step.
  2. Unclear guardrails — it's hard to restrict what the agent can do at each stage without building a bespoke multi-node graph.

SectionFlowMiddleware solves both by layering a lightweight state machine on top of create_react_agent. You describe sections in plain Python; the middleware handles everything else.


Features

Feature Description
Section-scoped tools Only the tools listed for the active section are visible to the model
Section-scoped prompts Phase instructions are injected as a prepended system message (prompt-cache friendly)
Auto-transitions Conditions evaluated before every model call advance the flow automatically
Agent-initiated transitions The built-in change_section tool lets the model move itself through the workflow
Per-section LLM Swap to a different model for a specific phase (e.g. a cheaper model for data-gathering)
Strict validation Optionally require specific state fields before entering a section
Fallback sections Gracefully recover when persisted state references a section that no longer exists
Global tool overrides Certain tools can span all sections and override section-level counterparts

Installation

Install the latest release from PyPI:

pip install langgraph-state-machine

You'll also need a model provider package for whichever LLM you use, e.g.:

pip install langchain-openai      # OpenAI
pip install langchain-anthropic   # Anthropic

Requirements: Python ≥ 3.10, langchain ≥ 1.0.0, langgraph ≥ 0.2.0

To hack on the library itself:

git clone https://github.com/mahmoud661/langgraph-state-machine
cd langgraph-state-machine
pip install -e ".[dev]"
pytest

How to use it

Using the middleware is four steps: define your tools, group them into sections, wrap the sections in a SectionFlowMiddleware, and pass that middleware to your agent. The package installs as section_flow:

import os

from langchain.agents import create_react_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from section_flow import SectionFlowMiddleware, SectionConfig

# --- 1. Define your tools ------------------------------------------------
@tool
def save_user_info(name: str, budget: int, category: str) -> str:
    """Save the user's name, budget, and product category."""
    return f"Saved: name={name}, budget={budget}, category={category}"

@tool
def search_catalog(query: str, max_price: int) -> str:
    """Search the product catalog and return matching items."""
    return f"Top results for '{query}' under ${max_price}: ..."

@tool
def process_payment(item_name: str, amount: float) -> str:
    """Process payment for the selected item."""
    return f"Payment of ${amount:.2f} for '{item_name}' processed."

# --- 2. Describe your workflow as sections -------------------------------
sections = {
    "gather": SectionConfig(
        name="gather",
        prompt=(
            "Collect the user's name, budget, and product category. "
            "Once you have all three, call save_user_info and transition to 'recommend'."
        ),
        tools=[save_user_info],
        allowed_transitions=["recommend"],
    ),
    "recommend": SectionConfig(
        name="recommend",
        prompt="Present exactly three options, then move to 'checkout' when the user picks one.",
        tools=[search_catalog],
        allowed_transitions=["checkout"],
    ),
    "checkout": SectionConfig(
        name="checkout",
        prompt="Confirm the item and total price, then process payment.",
        tools=[process_payment],
        allowed_transitions=[],  # terminal section
    ),
}

# --- 3. Create the middleware ---------------------------------------------
middleware = SectionFlowMiddleware(
    sections=sections,
    initial_section="gather",
    fallback_section="gather",
)

# --- 4. Attach it to your agent -------------------------------------------
agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"]),
    system_prompt="You are a friendly shopping assistant.",
    middleware=[middleware],
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Hi, I need a new laptop."}]}
)
print(result["current_section"])   # section the agent ended in
print(result["messages"][-1].content)

The agent starts in gather and only sees save_user_info plus the built-in change_section tool. When the model decides it has what it needs, it calls change_section(target_section="recommend") — from then on it sees the recommend prompt and tools instead. You never write graph nodes or edges; the middleware rewrites the prompt and tool list before every model call based on current_section in the agent state.

Runnable end-to-end scripts live in examples/.


Core concepts

SectionConfig

Each section is a pydantic.BaseModel:

SectionConfig(
    name="gather",                        # unique identifier
    prompt="...",                         # injected system message fragment
    tools=[my_tool],                      # tool objects available in this section
    allowed_transitions=["recommend"],    # sections this one may transition to
    required_state_fields={"budget": int},# fields that must exist in section_data before entering
    auto_transition_conditions=lambda state: (
        "recommend" if state.get("section_data", {}).get("budget") else None
    ),
    strict_validation=True,               # enforce allowed_transitions and required_state_fields
    on_enter=lambda state: None,          # lifecycle hook (called on entry)
    on_exit=lambda state: None,           # lifecycle hook (called on exit)
    llm=ChatOpenAI(model="gpt-4o-mini"), # optional per-section model override
    allowed_subagents=["research_agent"], # limit task-tool subagents for this section
)

SectionFlowState

The middleware extends your agent state with three fields:

class MyState(SectionFlowState):         # merge with your existing state
    messages: Annotated[list, add_messages]

# Fields added by the middleware:
# current_section  – name of the active section
# section_data     – shared dict for cross-section data (e.g. {"budget": 1000})
# visited_sections – ordered list of activated sections

Transitions

There are three ways to advance the flow:

Method When to use
Auto-transition Data-driven: move when section_data satisfies a condition
change_section tool Agent-driven: model explicitly calls the tool
before_model fallback Safety net: fallback section for missing/removed sections

Auto-transition (callable)

SectionConfig(
    name="gather",
    auto_transition_conditions=lambda state: (
        "recommend"
        if state.get("section_data", {}).get("budget")
        else None
    ),
    ...
)

Auto-transition (priority list)

from section_flow import TransitionCondition

SectionConfig(
    name="gather",
    auto_transition_conditions=[
        TransitionCondition(
            target="vip_recommend",
            condition=lambda s: s.get("section_data", {}).get("budget", 0) > 5000,
            priority=10,
        ),
        TransitionCondition(
            target="recommend",
            condition=lambda s: bool(s.get("section_data", {}).get("budget")),
            priority=0,
        ),
    ],
    ...
)

Per-section LLM

from langchain_openai import ChatOpenAI

SectionConfig(
    name="checkout",
    prompt="Process payment carefully.",
    tools=[process_payment],
    llm=ChatOpenAI(model="gpt-4o"),  # override the graph's default model here
)

Advanced usage

Pre-built SectionManager

Reuse the same manager across multiple agents:

from section_flow import SectionManager, SectionFlowMiddleware

manager = SectionManager(
    sections=sections,
    initial_section="gather",
    fallback_section="gather",
)

agent1 = create_react_agent(..., middleware=[SectionFlowMiddleware(section_manager=manager)])
agent2 = create_react_agent(..., middleware=[SectionFlowMiddleware(section_manager=manager)])

Global tools

Tools that should always be available regardless of section:

SectionFlowMiddleware(
    sections=sections,
    initial_section="gather",
    global_tools=[escalate_to_human],   # overrides section tools with same name
)

Disable the transition tool

If you prefer purely automatic or state-driven transitions:

SectionFlowMiddleware(
    sections=sections,
    initial_section="gather",
    include_transition_tool=False,
)

Runtime cache invalidation

If you modify sections at runtime (hot-reload), clear the tool cache:

middleware.clear_tool_cache()

API reference

SectionFlowMiddleware

Parameter Type Default Description
sections dict[str, SectionConfig] Section registry
initial_section str Starting section
strict_validation bool True Enforce transition and field rules
include_transition_tool bool True Register change_section tool
section_manager SectionManager None Pre-built manager (overrides above)
fallback_section str initial_section Fallback for removed sections
global_tools list [] Tools available in every section
subagent_graphs dict|list {} Subagent registry for task-tool filtering
all_middleware list [] Other middleware for string-name tool resolution

SectionConfig

Field Type Default Description
name str required Unique section identifier
prompt str required System message fragment injected when active
tools list [] Tool objects (or "task") available here
allowed_transitions list[str] [] Reachable sections (empty = all allowed)
required_state_fields dict[str, type] {} Fields required in section_data to enter
auto_transition_conditions callable or list None Conditions evaluated before each model call
strict_validation bool True Enforce rules for this section
on_enter callable None Called when section activates
on_exit callable None Called when section deactivates
llm any None Model override for this section
allowed_subagents list[str] None Subagents available via task tool

Examples

See the examples/ directory:

File What it demonstrates
01_basic_sections.py Three-section shopping assistant with agent-initiated transitions
02_auto_transitions.py Data-driven auto-transitions using TransitionCondition
03_per_section_llm.py Swapping models per section to balance quality and cost

How it works

User message
     │
     ▼
before_model()          ← initialise state, resolve fallbacks, fire auto-transitions
     │
     ▼
wrap_model_call()       ← prepend section prompt, filter tools, swap model if needed
     │
     ▼
LLM call
     │
     ▼
[agent calls change_section tool]  ← updates current_section in state
     │
     ▼
next before_model() ...

Contributing

See CONTRIBUTING.md.

License

Apache 2.0 — see LICENSE.

Release files for langgraph-state-machine 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langgraph-state-machine 0.1.1
File Size Uploaded
langgraph_state_machine-0.1.1.tar.gz 28.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langgraph-state-machine 0.1.1
File Interpreter ABI Platform
langgraph_state_machine-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 51.4 kB

Release files / langgraph_state_machine-0.1.1.tar.gz

Download URL langgraph_state_machine-0.1.1.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
daeda239af13171b41069ebd56c01980b46b6b8aa6779dea0afe926a4d5e91a8
BLAKE2b-256 checksum
How to use checksums
047a4ad3fd59566a8fde7b5e3b69bbd7b6bd3440b25028ab5bbaf2ea316621fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release files / langgraph_state_machine-0.1.1-py3-none-any.whl

Download URL langgraph_state_machine-0.1.1-py3-none-any.whl
Size 22.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d8b42ecd9ba3a3aedd595fa0320d67599465a323e0bf1c8bfae56cae6cb6622
BLAKE2b-256 checksum
How to use checksums
a44721295533cb2dd00068fb7904b2b53cb905b39ecc2f9b738dc431507488a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page