LangChain ToolRuntime: A three-way split for context, state, and store
1. What is ToolRuntime
ToolRuntime is a runtime object that LangChain automatically passes in when executing tools.
Tool parameters fall into two categories:
- Regular parameters: filled by the model based on the user question, e.g.,
keyword; runtime: injected automatically by the framework, the model cannot fill it.
from langchain.tools import ToolRuntime, tool
@tool
def search(keyword: str, runtime: ToolRuntime) -> str:
# keyword is filled by the model.
# runtime is passed in automatically by LangChain.
return f"Search: {keyword}"
2. Three core objects
| Object | Purpose | Typical contents |
|---|---|---|
context |
Fixed conditions for this run | user ID, permissions, database connections |
state |
Temporary state of the current flow | messages, steps, temporary results |
store |
Data saved across requests | user preferences, long-term memory |
Memory rule: context manages run conditions, state manages the current process, store manages long-term data.
3. context: passing in user and run dependencies
context is created by the program and then passed to the Agent. When user identity is needed, use information already verified by the login system; do not read the user ID from the user's text.
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import ToolRuntime, tool
@dataclass
class RequestContext:
# These values are passed in by the program; the model cannot modify them.
user_id: int
role: str
@tool
def show_user(runtime: ToolRuntime[RequestContext]) -> str:
# The tool reads user identity from context, and does not accept a user_id passed by the model.
return f"User {runtime.context.user_id}, role {runtime.context.role}"
# Configure the model according to the actual environment.
model = init_chat_model("openai:gpt-4o-mini")
# context_schema only declares the context type.
agent = create_agent(
model=model,
tools=[show_user],
context_schema=RequestContext,
)
# Real user information should come from the login system.
context = RequestContext(user_id=1001, role="user")
# The context parameter is what actually passes user information to the Agent.
result = agent.invoke(
{"messages": [{"role": "user", "content": "Query current user"}]},
context=context,
)
print(result["messages"][-1].content)
Use cases: permission checks, querying data by user, passing database sessions, passing tenant information.
Note: context_schema does not automatically create user information; you must first create RequestContext(...), then pass it in using context=....
4. state: temporary state of the current flow
state is what the Agent is currently processing. The simplest example is reading the current message count.
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import ToolRuntime, tool
@tool
def count_messages(runtime: ToolRuntime) -> str:
# messages is the message list built into Agent state.
# The second argument to get, [], is the default: returns an empty list when there is no messages.
messages = runtime.state.get("messages", [])
return f"There are currently {len(messages)} messages"
model = init_chat_model("openai:gpt-4o-mini")
agent = create_agent(
model=model,
tools=[count_messages],
)
# When the model calls the tool, ToolRuntime automatically carries the current state.
result = agent.invoke(
{"messages": [{"role": "user", "content": "Count the current messages"}]}
)
print(result["messages"][-1].content)
Use cases: reading messages, recording current steps, saving retry counts and temporary query results.
Real usage: counting model invocation times
The current code puts model_call_count into state, and a middleware increments it by 1 after each model call:
from typing import Any, NotRequired
from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime
class CustomAgentState(AgentState):
# AgentState already provides messages; here we only add a call counter.
model_call_count: NotRequired[int]
class ModelCallLimitMiddleware(AgentMiddleware[CustomAgentState]):
# Register the extended state type.
state_schema = CustomAgentState
def after_model(
self, state: CustomAgentState, runtime: Runtime
) -> dict[str, Any]:
# Start at 0 if not recorded, increment by 1 after each model call.
count = state.get("model_call_count", 0)
return {"model_call_count": count + 1}
When streaming, the updates event can read the state updated by the middleware:
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini")
agent = create_agent(
model=model,
middleware=[ModelCallLimitMiddleware()],
)
async def read_model_call_count() -> None:
input_state = {
"messages": [{"role": "user", "content": "Hello"}],
"model_call_count": 0,
}
model_call_count = 0
async for mode, data in agent.astream(
input_state,
stream_mode=["messages", "updates"],
):
if mode != "updates":
continue
update = data.get("ModelCallLimitMiddleware.after_model")
if update:
model_call_count = update["model_call_count"]
print(f"Model call count this round: {model_call_count}")
This count belongs only to the current Agent flow, suitable for logging, rate limiting, and debugging information.
5. store: saving data across requests
store is suitable for saving data that is still needed in the next request, for example:
- User language and theme preferences;
- Information the user explicitly asks to remember;
- Settings across sessions;
- Business caches that need to be reused.
It is not appropriate to unconditionally put all chat content into store. Before long-term saving, first judge whether the content has value, and isolate namespaces by user or tenant.
In real projects, store.put() and store.get() are usually wrapped into tools. The tool gets the current logged-in user ID from context, so external callers don't need to pass the user ID again:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import ToolRuntime, tool
from langgraph.store.memory import InMemoryStore
@dataclass
class UserContext:
# This ID should be passed in by the login system, not filled by the model.
user_id: str
@tool(description="Save the user's favorite color.")
def save_color(color: str, runtime: ToolRuntime[UserContext]) -> str:
# The tool saves data through runtime.store.
namespace = ("user", runtime.context.user_id)
runtime.store.put(
namespace,
"favorite_color",
{"value": color},
)
return "Color preference saved"
@tool(description="Read the user's previously saved favorite color.")
def read_color(runtime: ToolRuntime[UserContext]) -> str:
# When reading, use the same namespace and key.
namespace = ("user", runtime.context.user_id)
item = runtime.store.get(namespace, "favorite_color")
if item is None:
return "No color preference saved yet"
return f"Favorite color is: {item.value['value']}"
# The store should be created when the application starts so multiple requests can share it.
store = InMemoryStore()
model = init_chat_model("openai:gpt-4o-mini")
# Pass the store and context type to the Agent so tools can use runtime.store and runtime.context.
agent = create_agent(
model=model,
tools=[save_color, read_color],
context_schema=UserContext,
store=store,
)
# Pass the current logged-in user's context on each request.
context = UserContext(user_id="1001")
agent.invoke(
{"messages": [{"role": "user", "content": "Remember that I like blue"}]},
context=context,
)
# The next request still uses the same store, so the tool can read the previously saved content.
next_context = UserContext(user_id="1001")
result = agent.invoke(
{"messages": [{"role": "user", "content": "What color do I like?"}]},
context=next_context,
)
print(result["messages"][-1].content)
InMemoryStore only lives in memory; it is lost after a program restart. For permanent storage, switch to a persistence implementation such as a database.
7. How to choose
- User, permissions, and dependencies for this run: use
context. - Messages and temporary results for the current flow: use
state. - Data still needed in the next request: use
store.
Summary
ToolRuntime is the entry point for tools to obtain runtime information:
contextmanages the current run conditions;statemanages the current working process;storemanages cross-request data.
First determine the data's lifecycle, then choose the object, and your tool code will not become messy.