Files
langchain/libs/langchain_v1/pyproject.toml
Sydney Runkle 3b945d02d9 perf(langchain): stop inlining agent state into tool-dispatch Sends (#36960)
## Summary

Stop inlining the full agent state into every tool-dispatch `Send` in
`create_agent`. Dispatch with the bare list form `Send("tools",
[tool_call])` and let `ToolNode` hydrate `ToolRuntime.state` from graph
channels at tool-execution time.

**Depends on**
[langchain-ai/langgraph#7594](https://github.com/langchain-ai/langgraph/pull/7594)
which teaches `ToolNode` to read channel state via `CONFIG_KEY_READ`
when given a bare tool-call list. `uv.lock` pins that branch for CI
while the langgraph PR is in flight — this pin will be reverted to a
published `langgraph` version before merge.

## What was happening

Before this change, every pending tool call produced a `Send` whose
payload was:

```python
ToolCallWithContext(
    __type="tool_call_with_context",
    tool_call=tool_call,
    state=state,   # ← the FULL agent state dict, including messages list
)
```

For any agent that runs many turns, `state["messages"]` grows linearly
with the conversation. Every super-step that dispatches tools serializes
that whole list into every `Send`, and those Sends live forever in the
checkpointer's `__pregel_tasks` writes. The result is **O(N²)
`__pregel_tasks` storage** across a run.

## What changed

- `libs/langchain_v1/langchain/agents/factory.py`:
- `_make_model_to_tools_edge` now returns `Send("tools", [tool_call])` —
no inlined state.
  - Drops the `ToolCallWithContext` import.
- `libs/langchain_v1/pyproject.toml` + `libs/langchain_v1/uv.lock`:
- Temporary `[tool.uv.sources]` pin on `langgraph`,
`langgraph-prebuilt`, `langgraph-checkpoint` to the companion PR branch
so CI exercises both changes end-to-end. Revert after langgraph release.

## Why it's safe

- Same snapshot semantics as before. `Send` is emitted at the end of the
model super-step and consumed at the start of the tools super-step;
channels by that point reflect every write from the model super-step
(including the new AIMessage). Parallel tool tasks all see the same
values since sibling writes don't land until end-of-super-step.
- Legacy `ToolCallWithContext` input path is preserved in `ToolNode` —
no-op for any external caller still constructing it by hand.

## Test plan

- [x] `tests/unit_tests/agents/` — **738 passed, 2 skipped, 1 xfailed**
- [x] `ruff check .` / `ruff format .` — clean
- [x] `mypy langchain/agents/factory.py` — clean
- [x] Before/after benchmark (below)

## Benchmark

Script runs `create_agent` with a mock `GenericFakeChatModel` and two
tools (`write_file`, `edit_file`). Each of the N turns dispatches 2 tool
calls. After the run, the `InMemorySaver` is inspected for bytes stored
under `__pregel_tasks` — the channel that carries the tool-dispatch
`Send` payloads.

| N | TASKS before | TASKS after | ratio |
|---:|---:|---:|---:|
| 5   | 87.6 KB | **4.7 KB**  | **18.6× smaller**   |
| 10  | 335 KB  | **9.4 KB**  | **35.7× smaller**   |
| 25  | 2.05 MB | **23.7 KB** | **86.5× smaller**   |
| 50  | 8.14 MB | **47.6 KB** | **171× smaller**    |
| 100 | 32.5 MB | **95.3 KB** | **341× smaller**    |
| 200 | 130 MB  | **192 KB**  | **677× smaller**    |
| 500 | 815 MB  | **482 KB**  | **1,691× smaller**  |

**Growth shape:**

- **Before:** per-Send bytes scale with current `messages` length (full
state is inlined), so total TASKS across N turns = Σ(2 × k) for k=1..N ≈
O(N²).
- **After:** per-Send bytes are constant — just the `tool_call` dict.
Total TASKS is O(#dispatches), completely independent of conversation
length. In this bench with ~2 dispatches/turn: **940–964 bytes per turn
across N=5..500, essentially flat.**

An agent that makes 100 tool calls in a single turn pays the same TASKS
cost as one that makes 100 across 50 turns — which is the semantically
correct behavior.

Note: the `messages` channel is unchanged by this PR — it's still the
dominant storage term (growing O(N²) via `add_messages`). TASKS was a
second, compounding cost sitting on top of it; at N=100 it added 40% on
top of `messages`, at N=500 it added 67%. After the fix, TASKS is a
rounding error regardless of N.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:32:28 -04:00

200 lines
6.0 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "langchain"
description = "Building applications with LLMs through composability"
license = { text = "MIT" }
readme = "README.md"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Software Development :: Libraries :: Python Modules",
]
version = "1.2.15"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langchain-core>=1.3.2,<2.0.0",
"langgraph>=1.1.10,<1.2.0",
"pydantic>=2.7.4,<3.0.0",
]
[project.optional-dependencies]
community = ["langchain-community"]
anthropic = ["langchain-anthropic"]
openai = ["langchain-openai"]
azure-ai = ["langchain-azure-ai"]
#cohere = ["langchain-cohere"]
google-vertexai = ["langchain-google-vertexai"]
google-genai = ["langchain-google-genai"]
fireworks = ["langchain-fireworks"]
ollama = ["langchain-ollama"]
together = ["langchain-together"]
mistralai = ["langchain-mistralai"]
huggingface = ["langchain-huggingface"]
groq = ["langchain-groq"]
aws = ["langchain-aws"]
baseten = ["langchain-baseten>=0.2.0"]
deepseek = ["langchain-deepseek"]
xai = ["langchain-xai"]
perplexity = ["langchain-perplexity"]
[project.urls]
Homepage = "https://docs.langchain.com/"
Documentation = "https://reference.langchain.com/python/langchain/langchain/"
Repository = "https://github.com/langchain-ai/langchain"
Issues = "https://github.com/langchain-ai/langchain/issues"
Changelog = "https://github.com/langchain-ai/langchain/releases?q=tag%3A%22langchain%3D%3D1%22"
Twitter = "https://x.com/LangChain"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
"pytest>=9.0.3,<10.0.0",
"pytest-cov>=4.0.0,<8.0.0",
"pytest-watcher>=0.2.6,<1.0.0",
"pytest-asyncio>=1.3.0,<2.0.0",
"pytest-socket>=0.6.0,<1.0.0",
"pytest-xdist<4.0.0,>=3.6.1",
"pytest-mock",
"pytest-benchmark>=5.1.0,<6.0.0",
"syrupy>=5.0.0,<6.0.0",
"toml>=0.10.2,<1.0.0",
"blockbuster>=1.5.26,<1.6.0",
"langchain-tests",
"langchain-openai",
]
lint = [
"ruff>=0.15.0,<0.16.0",
]
typing = [
"mypy>=1.19.1,<1.20.0",
"types-toml>=0.10.8.20240310,<1.0.0.0",
]
test_integration = [
"vcrpy>=8.0.0,<9.0.0",
"wrapt>=1.15.0,<3.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"langchainhub>=0.1.16,<1.0.0",
"langchain-core",
"langchain-text-splitters",
]
[tool.uv]
constraint-dependencies = ["urllib3>=2.6.3", "pygments>=2.20.0"]
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
langchain-text-splitters = { path = "../text-splitters", editable = true }
langchain-openai = { path = "../partners/openai", editable = true }
langchain-anthropic = { path = "../partners/anthropic", editable = true }
[tool.ruff]
line-length = 100
[tool.mypy]
strict = true
enable_error_code = "deprecated"
warn_unreachable = true
exclude = [
# Exclude agents tests except middleware_typing/ which has type-checked tests
"tests/unit_tests/agents/middleware/",
"tests/unit_tests/agents/specifications/",
"tests/unit_tests/agents/test_.*\\.py",
]
# TODO: activate for 'strict' checking
warn_return_any = false
[[tool.mypy.overrides]]
module = ["pytest_socket.*", "vcr.*"]
ignore_missing_imports = true
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"ALL"
]
ignore = [
"C90", # McCabe complexity
"COM812", # Messes with the formatter
"CPY", # No copyright
"FIX002", # Line contains TODO
"PERF203", # Rarely useful
"PLR09", # Too many something (arg, statements, etc)
"TD002", # Missing author in TODO
"TD003", # Missing issue link in TODO
# TODO rules
"ANN401", # Any in type annotations
"BLE", # Blind exceptions
]
unfixable = [
"B028", # People should intentionally tune the stacklevel
]
flake8-annotations.allow-star-arg-any = true
allowed-confusables = [""]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.pydocstyle]
convention = "google"
ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters
[tool.ruff.lint.extend-per-file-ignores]
"tests/unit_tests/agents/*" = [
"ANN", # Annotations, needs to fix
"ARG", # Arguments, needs to fix
]
"tests/unit_tests/agents/test_responses_spec.py" = ["F821"]
"tests/unit_tests/agents/test_return_direct_spec.py" = ["F821"]
"tests/unit_tests/agents/test_react_agent.py" = ["ALL"]
"tests/*" = [
"D1", # Documentation rules
"S101", # Tests need assertions
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"SLF001", # Private member access in tests
"PLR2004", # Magic values are perfectly fine in unit tests (e.g. 0, 1, 2, etc.)
]
"scripts/*" = [
"INP", # Scripts are not in a package
"T201", # Scripts can print to the console
]
[tool.coverage.run]
omit = ["tests/*"]
[tool.pytest.ini_options]
addopts = "--strict-markers --strict-config --durations=5 --snapshot-warn-unused -vv"
markers = [
"requires: mark tests as requiring a specific library",
"scheduled: mark tests to run in scheduled testing",
"compile: mark placeholder test used to compile integration tests without running them",
"benchmark: mark benchmark tests",
]
asyncio_mode = "auto"
filterwarnings = [
"ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",
"ignore::langchain_core._api.deprecation.LangChainDeprecationWarning:tests",
"ignore::langchain_core._api.deprecation.LangChainPendingDeprecationWarning:tests",
]