Observed since 20 Oct, 21:42 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Modular node architecture for LangGraph agents
English | 日本語
📘 Full Documentation: https://yatarousan0227.github.io/agent-contracts/
🧩 Official Skills (agent instructions): docs/skills/official/index.md
Documentation
A modular, contract-driven node architecture for building scalable LangGraph agents.
Run the interactive tech support demo to see contract-driven routing and decision traces in action:
Demo guide: examples/interactive_tech_support/README.md
python -m examples.interactive_tech_support
This project is currently in Beta (Development Status :: 4 - Beta). Public APIs and the agent-contracts CLI are being stabilized ahead of 1.0; breaking changes will be documented in the changelog with migration notes.
Building multi-agent systems with raw graphs is powerful but scales poorly. As you add nodes, manual wiring (graph.add_edge) becomes unmanageable, routing logic gets scattered across conditional edges, and it becomes impossible to see how data flows through the system.
To build maintainable agent systems, we need to decouple node behavior (what it does) from graph topology (how it connects). We need a way to define strict interfaces without sacrificing the flexibility of LLM-based routing.
agent-contracts brings Contract-Driven Development to LangGraph.
Instead of manually wiring edges, you define a NodeContract for each agent—specifying its inputs, outputs, and when it should run. The framework then automatically compiles these contracts into a fully functional LangGraph, handling the complex routing, type-checking, and state management for you.

This library is designed for:
It is NOT for: Simple, linear chatbots or single-agent prototypes where raw LangChain/LangGraph suffices.
| Feature | Vanilla LangGraph | agent-contracts |
|---|---|---|
| Wiring | Manual add_edge & add_conditional_edges | Automatic based on Contracts |
| Routing | Custom logic inside conditional functions | Declarative TriggerConditions (Rule + LLM) |
| State Access | Unsafe dict access (state["key"]) | Type-safe StateAccessor pattern |
| Scalability | Hard to maintain as graph grows | Modular, nodes are self-contained |
| Observability | Standard Tracing | Enhanced, tracks why a node was picked |
graph TD
subgraph Definition
C[NodeContract] -->|Defines| N[ModularNode]
C -->|Specifies| I[Inputs/Outputs]
C -->|Specifies| T[TriggerConditions]
end
subgraph Build Time
R[NodeRegistry] -->|Collects| N
GB[GraphBuilder] -->|Reads| R
GB -->|Compiles to| LG[LangGraph]
end
subgraph Runtime
LG -->|Executes| S[Supervisor]
S -->|Evaluates| T
S -->|Routes to| N
end
The Supervisor automatically builds LLM context by:
request, response, _internalcontext_builder for application-specific contextDefine a simple node that just returns a value.
import asyncio
from agent_contracts import (
BaseAgentState,
ModularNode,
NodeContract,
NodeInputs,
NodeOutputs,
TriggerCondition,
)
from agent_contracts import get_node_registry, build_graph_from_registry
# 1. Define a Node
class HelloNode(ModularNode):
CONTRACT = NodeContract(
name="hello",
description="Returns a hello message",
reads=["request"],
writes=["response"],
supervisor="main",
trigger_conditions=[TriggerCondition(priority=100)], # Always trigger first
is_terminal=True, # End the flow after this node
)
async def execute(self, inputs: NodeInputs, config=None) -> NodeOutputs:
return NodeOutputs(
response={
"response_type": "done",
"response_message": "Hello World!",
}
)
async def main() -> None:
# 2. Register & Build
registry = get_node_registry()
registry.register(HelloNode)
graph = build_graph_from_registry(
registry=registry,
supervisors=["main"],
state_class=BaseAgentState,
)
graph.set_entry_point("main_supervisor") # required for LangGraph compilation
compiled = graph.compile()
# 3. Run
result = await compiled.ainvoke({"request": {"action": "start"}})
print(result["response"])
if __name__ == "__main__":
asyncio.run(main())
A more realistic setup with a rule-based trigger and an LLM-based trigger.
from agent_contracts import ModularNode, NodeContract, TriggerCondition
# Node A: Runs when user asks for "weather" (LLM semantic match)
class WeatherNode(ModularNode):
CONTRACT = NodeContract(
name="weather_agent",
description="Handles weather-related requests",
reads=["request"],
writes=["response"],
supervisor="main",
requires_llm=True,
trigger_conditions=[
TriggerCondition(
llm_hint="User is asking about the weather forecast",
priority=10
)
]
)
# ... implementation ...
# Node B: Runs when a strict flag is present (Rule match)
class UrgentNode(ModularNode):
CONTRACT = NodeContract(
name="urgent_agent",
description="Handles urgent/high-priority requests",
reads=["request"],
writes=["response"],
supervisor="main",
trigger_conditions=[
TriggerCondition(
when={"request.priority": "high"},
priority=20 # Checked BEFORE LLM
)
]
)
# ... implementation ...
Validate, visualize, and diff contracts from your registered nodes:
agent-contracts validate --module myapp.nodes --strict
agent-contracts visualize --module myapp.nodes --output ARCHITECTURE.md
agent-contracts diff --from-module myapp.v1.nodes --to-module myapp.v2.nodes
See docs/cli.md for details.
examples/05_backend_runtime.py: backend-oriented runtime with strict validationexamples/03_simple_chatbot.py: minimal rule-based routingexamples/04_multi_step_workflow.py: sequential workflow patternexamples/interactive_tech_support/: interactive multi-node demo (routing trace + optional LLM). Run: python -m examples.interactive_tech_supportNodeContract.The contract is the source of truth for a node.
NodeContract(
name="my_node",
description="Calculates mortgage payments",
reads=["user_profile", "loan_data"],
writes=["payment_schedule"],
requires_llm=True, # Whether LLM is required
supervisor="main", # Which supervisor manages this node
trigger_conditions=[
TriggerCondition(llm_hint="User asks about monthly payments")
]
)
The supervisor handles the control flow:
when conditions.llm_hints.For conversational agents, you can extend InteractiveNode, which provides a structured way to handle turns, generate questions, and process answers.
from agent_contracts import InteractiveNode
class InterviewNode(InteractiveNode):
CONTRACT = NodeContract(
name="interview",
description="Conversational workflow node",
reads=["request", "_internal"],
writes=["response", "_internal"],
supervisor="main",
trigger_conditions=[
TriggerCondition(priority=10, llm_hint="Use for conversational workflows"),
],
)
def prepare_context(self, inputs):
"""Extract context from inputs."""
return {"interview_state": inputs.get_slice("interview")}
def check_completion(self, context, inputs):
"""Check if interview is complete."""
return context["interview_state"].get("complete", False)
async def process_answer(self, context, inputs):
"""Process user's answer."""
# Handle the answer logic
return True
async def generate_question(self, context, inputs):
"""Generate next question."""
return NodeOutputs(
response={
"response_type": "question",
"response_data": {"question": "..."},
}
)
Avoid stringly-typed state access. StateAccessor provides a safe way to read and write state slices.
from agent_contracts import Internal, reset_response
# Bad
user_id = state["profile"]["id"]
# Good (agent-contracts)
user_id = Internal.user_id.get(state)
# Writing (returns new state)
state = Internal.turn_count.set(state, 5)
state = reset_response(state)
By default, GenericSupervisor passes only request, response, and _internal slices to the LLM for routing decisions. For complex scenarios requiring additional context (e.g., conversation history, domain state), you can provide a custom context_builder.
The Supervisor automatically sanitizes long field values to prevent large binary data (e.g., base64 images) from being included in LLM prompts:
supervisor = GenericSupervisor(
supervisor_name="shopping",
llm=llm,
max_field_length=10000 # Default: 10000 characters
)
image, iVBOR, /9j/, R0lGOD, data:image) are replaced with [IMAGE_DATA]max_field_length characters and append ...[TRUNCATED:{n}_chars]from agent_contracts import GenericSupervisor
def ecommerce_context_builder(state: dict, candidates: list[str]) -> dict:
"""Build context for e-commerce routing decisions."""
cart = state.get("cart", {})
inventory = state.get("inventory", {})
return {
"slices": {"request", "response", "_internal", "cart", "inventory"},
"summary": {
"cart_total": sum(item["price"] for item in cart.get("items", [])),
"low_stock_count": len([i for i in inventory.get("items", [])
if i["quantity"] < 10]),
"user_tier": state.get("user", {}).get("tier", "standard"),
},
}
supervisor = GenericSupervisor(
supervisor_name="checkout",
llm=llm,
registry=registry,
context_builder=ecommerce_context_builder,
)
def conversation_context_builder(state: dict, candidates: list[str]) -> dict:
"""Build context with conversation history."""
messages = state.get("conversation", {}).get("messages", [])
user_messages = [m for m in messages if m.get("role") == "user"]
return {
"slices": {"request", "response", "_internal", "conversation"},
"summary": {
"total_turns": len(user_messages),
"last_question": messages[-2].get("content") if len(messages) >= 2 else None,
"last_answer": messages[-1].get("content") if messages else None,
},
}
supervisor = GenericSupervisor(
supervisor_name="assistant",
llm=llm,
context_builder=conversation_context_builder,
)
See ContextBuilder protocol in the API documentation for full details.
For production applications, use the Runtime Layer for unified execution, lifecycle hooks, and streaming.
Standard request/response execution.
from agent_contracts import AgentRuntime, RequestContext, InMemorySessionStore
runtime = AgentRuntime(
graph=compiled_graph,
session_store=InMemorySessionStore(),
)
result = await runtime.execute(RequestContext(
session_id="abc123",
action="answer",
message="I like casual style",
resume_session=True, # Loads state from store
))
print(result.response_type) # "interview", "proposals", etc.
print(result.response_data) # Response payload
Supports Server-Sent Events (SSE) streaming, yielding events as each node executes.
from agent_contracts.runtime import StreamingRuntime
runtime = (
StreamingRuntime()
.add_node("search", search_node, "Searching...")
.add_node("stylist", stylist_node, "Generating recommendations...")
)
async for event in runtime.stream(request):
yield event.to_sse()
Implement protocols to customize behavior.
from agent_contracts import RuntimeHooks, SessionStore
class MyHooks(RuntimeHooks):
async def prepare_state(self, state, request):
# Normalize or enrich state before execution
return state
async def after_execution(self, state, result):
# Persist session, log, etc.
pass
pip install agent-contracts
# or from source
pip install git+https://github.com/yatarousan0227/agent-contracts.git
Manage agent behavior without changing code.
# agent_config.yaml
supervisor:
max_iterations: 10
io:
# Contract I/O enforcement (runtime)
strict: false # true: raise ContractViolationError
warn: true # log warnings on violations
drop_undeclared_writes: true # drop undeclared writes by default
response_types:
terminal_states: ["done", "error"]
features: {}
from agent_contracts.config import load_config
config = load_config("agent_config.yaml")
agent-contracts is fully integrated with LangSmith for deep tracing.
LangChain API keys must be set:
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="..."
Generate professional documentation from your code.
from agent_contracts import ContractVisualizer
visualizer = ContractVisualizer(registry, graph=compiled)
doc = visualizer.generate_architecture_doc()
| Section | Description |
|---|---|
| 📦 State Slices | All slices with readers/writers + ER diagram |
| 🔗 LangGraph Node Flow | Mermaid visualization of the compiled LangGraph |
| 🎯 System Hierarchy | Supervisor-Node structure with Mermaid flowchart |
| 🔀 Data Flow | Node dependencies via shared slices |
| ⚡ Trigger Hierarchy | Priority-ordered triggers (🔴 high → 🟢 low) |
| 📚 Nodes Reference | Complete node details table |
You can also generate sections individually:
print(visualizer.generate_langgraph_flow())
print(visualizer.generate_state_slices_section())
See ARCHITECTURE_SAMPLE.md for example output.
| Export | Description |
|---|---|
ModularNode | Base class for all nodes |
InteractiveNode | Base class for conversational nodes |
NodeContract | Node I/O contract definition |
TriggerCondition | Trigger condition for routing |
NodeInputs / NodeOutputs | Typed I/O containers |
NodeRegistry | Node registration and discovery |
GenericSupervisor | LLM-driven routing supervisor |
GraphBuilder | Automatic LangGraph construction |
BaseAgentState | Base state class with slices |
ContractVisualizer | Architecture document generator |
| Export | Description |
|---|---|
AgentRuntime | Unified execution engine with lifecycle hooks |
StreamingRuntime | Node-by-node streaming for SSE |
RequestContext | Execution request container |
ExecutionResult | Execution result with response |
RuntimeHooks | Protocol for customization hooks |
SessionStore | Protocol for session persistence |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the Mozilla Public License 2.0 (MPL-2.0) - see the LICENSE file for details.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.6.0 | 31 Jan 2026 | 9 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 21:42 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:0741173edf1f36c4cbb88667780bb5f4c1bead3caf8f2f6ab24a7675ecaf4531