LangGraph Section Flow Middleware
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:
- Context pollution — the agent is distracted by tools and instructions that are irrelevant to the current step.
- 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
pip install langgraph-state-machine
Requirements: Python ≥ 3.10, langchain ≥ 1.0.0, langgraph ≥ 0.2.0
Quick start
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent
from section_flow import SectionFlowMiddleware, SectionConfig
# --- Define your tools (stubs for illustration) ---
def collect_requirements(query: str) -> str: ...
def search_catalog(query: str) -> str: ...
def process_payment(amount: float) -> str: ...
# --- Configure sections ---
sections = {
"gather": SectionConfig(
name="gather",
prompt=(
"Your goal is to understand the user's needs. "
"Ask for their name, budget, and product category before moving on."
),
tools=[collect_requirements],
allowed_transitions=["recommend"],
),
"recommend": SectionConfig(
name="recommend",
prompt="Suggest the three best products that match the user's stated budget and category.",
tools=[search_catalog],
allowed_transitions=["checkout"],
),
"checkout": SectionConfig(
name="checkout",
prompt="Guide the user through payment. Confirm the amount before charging.",
tools=[process_payment],
),
}
# --- Build the agent ---
agent = create_react_agent(
model="openai:gpt-4o",
system_prompt="You are a friendly shopping assistant.",
middleware=[
SectionFlowMiddleware(
sections=sections,
initial_section="gather",
)
],
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hi, I need a new laptop."}]})
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langgraph_state_machine-0.1.0.tar.gz.
File metadata
- Download URL: langgraph_state_machine-0.1.0.tar.gz
- Upload date:
- Size: 27.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47f58facc40f5a0782f9eb2364bfc629f3fb88ded638154def9ff91195322cf5
|
|
| MD5 |
9be94d5e589659201fb2412ead0e3b1b
|
|
| BLAKE2b-256 |
0f62bfbd29ce480ff89d9a29ef882e3c48d66aaead0686e910c91be79f869c04
|
Provenance
The following attestation bundles were made for langgraph_state_machine-0.1.0.tar.gz:
Publisher:
release.yml on mahmoud661/langgraph-state-machine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_state_machine-0.1.0.tar.gz -
Subject digest:
47f58facc40f5a0782f9eb2364bfc629f3fb88ded638154def9ff91195322cf5 - Sigstore transparency entry: 2339863850
- Sigstore integration time:
-
Permalink:
mahmoud661/langgraph-state-machine@70f0bcd808dc23d3e20ae59a72aeefc8ccf3fb9d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mahmoud661
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@70f0bcd808dc23d3e20ae59a72aeefc8ccf3fb9d -
Trigger Event:
push
-
Statement type:
File details
Details for the file langgraph_state_machine-0.1.0-py3-none-any.whl.
File metadata
- Download URL: langgraph_state_machine-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32b5a66b1daec9943caae9556b93a9dd6867643c71dd970c351d41fa947dc86e
|
|
| MD5 |
1b5eea8a23469e6311ce1b92ead6b936
|
|
| BLAKE2b-256 |
39b9f609327e08fbf5a75c7260a5032a89ed36cfd780f1d10043cc2e6eb0364a
|
Provenance
The following attestation bundles were made for langgraph_state_machine-0.1.0-py3-none-any.whl:
Publisher:
release.yml on mahmoud661/langgraph-state-machine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_state_machine-0.1.0-py3-none-any.whl -
Subject digest:
32b5a66b1daec9943caae9556b93a9dd6867643c71dd970c351d41fa947dc86e - Sigstore transparency entry: 2339863862
- Sigstore integration time:
-
Permalink:
mahmoud661/langgraph-state-machine@70f0bcd808dc23d3e20ae59a72aeefc8ccf3fb9d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mahmoud661
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@70f0bcd808dc23d3e20ae59a72aeefc8ccf3fb9d -
Trigger Event:
push
-
Statement type: