Composable middleware framework for LangGraph agents
Project description
langchain-agentkit
Composable middleware framework for LangGraph agents.
Build LangGraph agents with reusable middleware that composes tools and prompts. Two layers to choose from:
agentmetaclass — declare a class, get a complete ReAct agent with middleware-composed tools and promptsAgentKit— primitive composition engine for full control over graph topology
Installation
Requires Python 3.11+, langchain-core>=0.3, langgraph>=0.4.
pip install langchain-agentkit
Quick Start
The agent metaclass (recommended)
Declare a class that inherits from agent to get a StateGraph with an automatic ReAct loop:
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_agentkit import agent, SkillsMiddleware, TasksMiddleware
class researcher(agent):
llm = ChatOpenAI(model="gpt-4o")
tools = [web_search]
middleware = [SkillsMiddleware("skills/"), TasksMiddleware()]
prompt = "You are a research assistant."
async def handler(state, *, llm, prompt):
messages = [SystemMessage(content=prompt)] + state["messages"]
response = await llm.ainvoke(messages)
return {"messages": [response], "sender": "researcher"}
# Compile and use
graph = researcher.compile()
result = graph.invoke({"messages": [HumanMessage("Size the B2B SaaS market")]})
# With checkpointer for interrupt() support
from langgraph.checkpoint.memory import InMemorySaver
graph = researcher.compile(checkpointer=InMemorySaver())
AgentKit for manual graph wiring
Use AgentKit when you need full control over graph topology — multi-node graphs, shared ToolNode, custom routing:
from langchain_agentkit import AgentKit, SkillsMiddleware, TasksMiddleware
kit = AgentKit([
SkillsMiddleware("skills/"),
TasksMiddleware(),
])
# In any graph node:
all_tools = my_tools + kit.tools
system_prompt = kit.prompt(state, runtime)
Standalone SkillRegistry
Use SkillRegistry directly for skill discovery without the middleware layer:
from langchain_agentkit import SkillRegistry
registry = SkillRegistry("skills/")
tools = registry.tools # [Skill, SkillRead]
Examples
See examples/ for complete working code:
standalone_node.py— Simplest usage: declare a node class, compile, invokemanual_wiring.py— UseSkillRegistryas a standalone toolkit with full graph controlmulti_agent.py— Compose multiple agents in a parent graphroot_with_checkpointer.py— Multi-turn conversations withinterrupt()andCommand(resume=...)subgraph_with_checkpointer.py— Subgraph inherits parent's checkpointer automaticallycustom_state_type.py— Custom state shape via handler annotation + subgraph schema translation
API Reference
agent
Declarative agent builder. Subclassing produces a StateGraph. Call .compile() to get a runnable graph.
Class attributes:
| Attribute | Required | Description |
|---|---|---|
llm |
Yes | Language model instance |
tools |
No | List of LangChain tools |
middleware |
No | Ordered list of Middleware instances |
prompt |
No | System prompt — inline string, file path, or list of either |
Handler signature:
async def handler(state, *, llm, tools, prompt, runtime): ...
state is positional. Everything after * is keyword-only and injected by name — declare only what you need:
| Parameter | Type | Description |
|---|---|---|
state |
dict |
LangGraph state (positional, required) |
llm |
BaseChatModel |
LLM pre-bound with all tools via bind_tools() |
tools |
list[BaseTool] |
All tools (user tools + middleware tools) |
prompt |
str |
Fully composed prompt (template + middleware sections) |
runtime |
ToolRuntime |
Unified runtime context. Use runtime.config for the full RunnableConfig |
Both sync and async handlers are supported — sync handlers are detected via inspect.isawaitable and awaited automatically.
Custom state types — annotate the handler's state parameter:
class MyState(TypedDict, total=False):
messages: Annotated[list, add_messages]
draft: dict | None
class my_agent(agent):
llm = ChatOpenAI(model="gpt-4o")
async def handler(state: MyState, *, llm):
...
Without an annotation, AgentState is used by default.
Middleware protocol
Any class with tools (property) and prompt(state, runtime) (method) satisfies the protocol via structural subtyping — no base class needed:
class MyMiddleware:
@property
def tools(self) -> list[BaseTool]:
return [my_tool]
def prompt(self, state: dict, runtime: ToolRuntime) -> str | None:
return "You have access to my_tool."
Built-in middleware:
| Middleware | Tools | Prompt |
|---|---|---|
SkillsMiddleware(skills_dirs) |
Skill, SkillRead |
Progressive disclosure skill list with load instructions |
TasksMiddleware() |
TaskCreate, TaskUpdate, TaskList, TaskGet |
Base agent behavior + task context with status icons |
TasksMiddleware
Task management middleware with Command-based tools that update graph state via LangGraph's ToolNode.
# Default — auto-creates task tools
mw = TasksMiddleware()
# Custom tools
mw = TasksMiddleware(task_tools=[my_create, my_update])
# Custom task formatter
mw = TasksMiddleware(formatter=my_format_function)
Task tools use InjectedState to read tasks from state and return Command(update={"tasks": [...]}) to apply changes. Task state is updated locally within the agent's graph, visible to the prompt on every ReAct loop iteration. When used as a subgraph, state flows back to the parent graph on completion.
You can also create task tools directly:
from langchain_agentkit import create_task_tools
tools = create_task_tools() # [TaskCreate, TaskUpdate, TaskList, TaskGet]
AgentKit(middleware, prompt=None)
Composition engine that merges tools and prompts from middleware.
tools— All tools from all middleware, deduplicated by name (first middleware wins, cached)prompt(state, runtime)— Template + middleware sections, joined with double newline (dynamic per call)
Prompt templates can be inline strings, file paths, or a list of either:
kit = AgentKit(middleware, prompt="You are helpful.")
kit = AgentKit(middleware, prompt=Path("prompts/system.txt"))
kit = AgentKit(middleware, prompt=["prompts/base.txt", "Extra instructions"])
node
Skill-aware metaclass. Uses skills attribute instead of middleware. Consider using agent for the full middleware composition model.
class my_agent(node):
llm = ChatOpenAI(model="gpt-4o")
tools = [web_search]
skills = "skills/" # str, list[str], or SkillRegistry instance
async def handler(state, *, llm, tools, runtime): ...
AgentState
Minimal LangGraph state type with task support:
| Field | Type | Description |
|---|---|---|
messages |
Annotated[list, add_messages] |
Conversation history with LangGraph message reducer |
sender |
str |
Name of the last node that produced output |
tasks |
list[dict[str, Any]] |
Task list managed by TasksMiddleware tools |
Extend with your own fields:
class MyState(AgentState):
current_project: str
iteration_count: int
Security
- Path traversal prevention: Skill file paths resolved to absolute and checked against skill directories. Reference file names reject
.and..patterns. - Name validation: Skill names validated per AgentSkills.io spec — lowercase alphanumeric + hyphens, 1-64 chars.
- Tool scoping: Each agent only has access to the tools declared in its
toolsattribute plus middleware-provided tools. - Prompt trust boundary: Prompt templates and middleware prompt sections are set by the developer at construction time, not by end-user input.
Contributing
git clone https://github.com/rsmdt/langchain-agentkit.git
cd langchain-agentkit
uv sync --extra dev
uv run pytest --tb=short -q
uv run ruff check src/ tests/
uv run mypy src/
Project details
Release history Release notifications | RSS feed
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 langchain_agentkit-0.5.0.tar.gz.
File metadata
- Download URL: langchain_agentkit-0.5.0.tar.gz
- Upload date:
- Size: 126.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47e1d0286bb58f0fd9f5cad133c24678054b55ba5cfc965dff3736d16b950966
|
|
| MD5 |
a2ac64172d775f444b72b80be90e344d
|
|
| BLAKE2b-256 |
d8c39418da935acf2dd5007b17cbae16bd278bac9dd7aea67dca8cb19312a2da
|
File details
Details for the file langchain_agentkit-0.5.0-py3-none-any.whl.
File metadata
- Download URL: langchain_agentkit-0.5.0-py3-none-any.whl
- Upload date:
- Size: 32.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d965f76c6ef38cf58c74a35e85b03059c9001d54b2fbc27622a1ae438d0ca927
|
|
| MD5 |
2dcc0981c38fccab563ebe4f91e27fc4
|
|
| BLAKE2b-256 |
ca253e2b52cf81c32fe1b9244b06c12214ccf73823ea84b966d338a116152906
|