Compare commits

...
Author SHA1 Message Date
cwlbraaandGitHub f15ec34132 Update OpenAPI spec from LangGraph API v0.5.17 2025-11-19 19:51:19 +00:00
Sydney RunkleandGitHub 6d20a0b9c7 fix: deprecate setattr on ToolCallRequest (#6462)
* one alternative considered was setting `frozen=True` on the dataclass,
but this is breaking, so a deprecation is a nicer approach
2025-11-19 13:12:11 -05:00
William FHandGitHub df8becd5cf refactor: separate prepare_push_* functions (#6450)
Extract two common cases from the big switch statement of
`prepare_single_task` since it's a tad more composable.

All this does is shift/extract code to separate functions
2025-11-14 16:13:03 -08:00
Lauren Hirata SinghandGitHub 056ba91a71 chore(docs): add more redirects + catchall (#6442) 2025-11-13 14:49:40 -05:00
Sydney RunkleandGitHub 02300de24c fix: dep warnings in prebuilt (#6443) 2025-11-13 13:59:34 -05:00
Sydney RunkleandGitHub ac16bdb795 release: prebuilt 1.0.3 (#6441) 2025-11-13 13:38:24 -05:00
Caspar BroekhuizenandGitHub 0d4ac836e3 chore: langgraph patch release (#6429) 2025-11-10 09:37:35 -08:00
Lauren Hirata SinghandGitHub 201c8015ea chore(docs): Update links in notebook_hooks.py for deployment (#6428) 2025-11-10 09:51:05 -05:00
Mason DaughertyandGitHub cf3e8252f5 feat(docs): warn that StateGraph is a builder class (#6417) 2025-11-07 21:09:15 -05:00
Mason DaughertyandGitHub 7a5e3c1e79 fix(docs): PartialState rendering in MkDocs (#6416)
The carat chars were not rendering without code style formatting
2025-11-07 21:08:01 -05:00
Mason DaughertyandGitHub 218c60717e fix(docs): synchronize invoke and ainvoke docstrings (#6415)
Similar to #6414
2025-11-07 20:44:33 -05:00
Mason DaughertyandGitHub 28b9f578b0 fix(docs): synchronize stream and astream docstrings (#6414)
`stream` and `astream` docstrings listed different available
`stream_mode` options.

Both methods support the same seven stream modes as defined in
`StreamMode`

Fixed for consistency
2025-11-07 20:44:25 -05:00
le-codeur-rapideandGitHub bef76b791c docs(langgraph): Fix docstring code examples of task function (#6410)
Hi all,
I found out that the sync and async code examples of the `task` function
in `libs/langgraph/langgraph/func/__init__.py` have a typo:
```
    Example: Sync Task
        ```python
        from langgraph.func import entrypoint, task


        @task
        def add_one(a: int) -> int:
            return a + 1


        @entrypoint()
        def add_one(numbers: list[int]) -> list[int]:
            futures = [add_one(n) for n in numbers]
            results = [f.result() for f in futures]
            return results


        # Call the entrypoint
        add_one.invoke([1, 2, 3])  # Returns [2, 3, 4]
        ```
```

Both task and entrypoint functions have the same name which gives an
error.

This is a small PR to fix this
2025-11-07 16:01:19 -05:00
a1b34efdb0 fix(checkpoint-postgres): ensure vector extension is created only if not exists (#6154)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

- [x] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
  - Examples:
    - feat(core): add multi-tenant support
    - fix(cli): resolve flag parsing error
    - docs(openai): update API usage examples
  - Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore,
revert, release
  - Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres,
checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do
not include it in the PR.

- **Description:** Azure Postgres SQL server has a limitation when doing
create extension vector is not exists, even though it manually created
before on a schema.
- **Issue:** Even though `CREATE EXTENSION vector` is executed manually
before, the permission issue arises. Putting it in an if else block
solves the issue and its not a breaking change.
```
Because vector isn't a trusted extension, only members of "azure_pg_admin" are allowed to use CREATE EXTENSION vector
HINT: to learn how to allow an extension or see the list of allowed extensions, please refer to https://go.microsoft.com/fwlink/?linkid=2301063
```

Co-authored-by: Josh Rogers <josh@langchain.dev>
2025-11-07 11:42:15 -05:00
André MenezesandGitHub 7ab5788f25 fix(langgraph): Unexpected behavior for stream_mode sequences that are not lists (#6354)
## Issue
The `stream_mode` argument type includes `Sequence`, but it doesn't
correctly support non-list sequences. On the other hand, the
`print_mode` argument works as expected.

### Example
```python
from langgraph.pregel.main import Pregel

pregel = Pregel(nodes={}, channels=None, input_channels=[], output_channels=[], auto_validate=False)
stream_modes, *_ = pregel._defaults(
    config={"recursion_limit": 1},
    stream_mode=("values", "messages"),
    print_mode=("values"),
    output_keys=None,
    interrupt_before=None,
    interrupt_after=None,
    durability=None,
)
print(stream_modes) # Expected `{'values', 'messages'}`, got `{('values', 'messages'), 'values'}`
```
2025-11-07 08:01:25 -05:00
Cole MurrayandGitHub b0a1029d55 fix(checkpoint-postgres): Replace f-string SQL formatting with parameterized queries in migration statements (#6328)
## Summary

Replace f-string SQL formatting with parameterized queries to prevent
potential SQL injection in checkpoint migration code.

## Changes

Updated the migration version tracking INSERT statements in all
checkpoint saver classes to use parameterized queries instead of
f-string formatting:

- `PostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py:100)
- `AsyncPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py:104-106)
- `ShallowPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py:255)
- `AsyncShallowPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py:617-619)

**Before (vulnerable to SQL injection):**
```python
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
```

**After (using parameterized query):**
```python
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
```

## Risk Assessment

The practical risk is low since `v` is an integer loop variable
controlled by the codebase. However, using string formatting in SQL
queries is a well-known anti-pattern that can lead to SQL injection
vulnerabilities, especially if the code is later refactored or copied to
other contexts.

## Testing

-  All 216 tests passing on PostgreSQL 15 and 16
-  Linting and type checking passing
-  No functional changes to behavior
2025-11-07 08:00:10 -05:00
Mason DaughertyandGitHub c2ef3f3fd3 fix: remove SDK inline links (#6307)
these were broken; remove for now.
2025-11-07 07:56:00 -05:00
Michael LiandGitHub d455bd841d fix: fix previoius edge cases such as 0 (#6379)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

- [x] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
  - Examples:
    - feat(core): add multi-tenant support
    - fix(cli): resolve flag parsing error
    - docs(openai): update API usage examples
  - Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore,
revert, release
  - Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres,
checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do
not include it in the PR.

- [x] **PR message**: ***Delete this entire checklist*** and replace
with
- **Description:** a description of the change. Include a [closing
keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
if applicable.
  - **Issue:** the issue # it fixes, if applicable
  - **Dependencies:** any dependencies required for this change
- **Twitter handle:** if your PR gets announced, and you'd like a
mention, we'll gladly shout you out!

- [x] **Add tests and docs**: If you're adding a new integration, you
must include:
1. A test for the integration, preferably unit tests that do not rely on
network access,
2. An example notebook showing its use. It lives in
`docs/docs/integrations` directory.

- [x] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. We will not consider a
PR unless these three are passing in CI. See [contribution
guidelines](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md)
for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
2025-11-07 07:54:22 -05:00
Pedro Enrique Agurto CastilloandGitHub 69a09adef6 fix(langgraph): export REMOVE_ALL_MESSAGES in __all__ to fix linting (#6375)
`REMOVE_ALL_MESSAGES` is a public constant used with `RemoveMessage` to
clear all messages from the state:

```python
from langchain_core.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES

# Clear all messages
[RemoveMessage(id=REMOVE_ALL_MESSAGES)]
```

However, it is not exported in __all__, causing:

Linting errors in IDEs (PyCharm)
no-member warnings from Pylint
Confusion for users

This PR:

Adds REMOVE_ALL_MESSAGES to __all__
Adds inline docstring with usage example

No runtime behavior changes — only improves IDE support and API clarity.

Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

---
Local verification:
```bash
# Before
from langgraph.graph.message import REMOVE_ALL_MESSAGES  # Pylint: no-member

# After: no error
```
CI Note: This is a pure export/docs fix. `make lint` and `make test`
pass unchanged.
2025-11-07 07:52:27 -05:00
Kavya GoyalandGitHub 35aa98b110 fix(sdk-py): use correct f-string representation when loading error (#6388)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**


## Description
- Fixed a bug in `libs/sdk-py/langgraph_sdk/auth/__init__.py` where the
error message for an already-set authentication handler did not properly
render the handler value.
- Updated the error string from a static `{self._authenticate_handler}`
to a correctly interpolated f-string.

```python
"Authentication handler already set as {self._authenticate_handler}."
```

```python
f"Authentication handler already set as {self._authenticate_handler}."
```

- Error messages now correctly display the actual handler instance,
improving debugging clarity.



- **Issue:** Fixes https://github.com/langchain-ai/langgraph/issues/6387
  - **Dependencies:** -
  - **Twitter handle:** -

- [x] **Add tests and docs**: If you're adding a new integration, you
must include:
1. A test for the integration, preferably unit tests that do not rely on
network access,
2. An example notebook showing its use. It lives in
`docs/docs/integrations` directory.

- [ ] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. We will not consider a
PR unless these three are passing in CI. See [contribution
guidelines](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md)
for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
2025-11-07 07:51:35 -05:00
Mason DaughertyandGitHub 0f83d9fafe style: update docstrings to reference StateGraph (#6308)
nit
2025-11-07 07:47:29 -05:00
Logan RosenandGitHub 52d66df92c docs(langgraph): update streaming guide links (#6314)
Updating links to the LangGraph streaming guide to point to the new
documentation website for 1.0.
2025-11-07 07:46:48 -05:00
Mason DaughertyandGitHub 4ec92f9fb1 chore: add pyproject.toml links (#6364) 2025-11-07 07:43:51 -05:00
inhunandGitHub 2b72953064 docs: add license files for checkpoint-sqlite and checkpoint-postgres (#6392)
In this PR:

- Add missing LICENSE files for checkpoint-sqlite and
checkpoint-postgres libraries.

Both libraries specify the MIT License in their pyproject.toml files,
but the actual LICENSE files were missing.
This update adds the corresponding LICENSE files to ensure proper
license documentation and compliance.
2025-11-07 07:39:06 -05:00
le-codeur-rapideandGitHub 232014e8ef docs(langgraph): Fix typo in docstring of PregelLoop.tick (#6407)
This is a very small PR to correct a typo in the docstring of the
`PregelLoop.tick()` method.
```python
  def tick(self) -> bool:
      """Execute a single iteration of the Pregel loop.

      Args:
          input_keys: The key(s) to read input from.

      Returns:
          True if more iterations are needed.
      """
```

Corrected to :
```python
  def tick(self) -> bool:
      """Execute a single iteration of the Pregel loop.

      Returns:
          True if more iterations are needed.
      """
```

The docstring was written in #2946 when the signature of tick was
```python
    def tick(
        self,
        *,
        input_keys: Union[str, Sequence[str]],
    ) -> bool:
```
but  it was simplified to 
```python
def tick(self) -> bool:
```
in #5080
2025-11-07 07:38:16 -05:00
33 changed files with 1014 additions and 786 deletions
+48 -2
View File
@@ -129,11 +129,25 @@ REDIRECT_MAP = {
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
# LGP mintlify migration redirects
"examples/index.md": "https://docs.langchain.com/oss/python/learn",
"guides/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"tutorials/index.md": "https://docs.langchain.com/oss/python/learn",
"llms-txt-overview.md": "https://docs.langchain.com/llms.txt",
"tutorials/rag/langgraph_adaptive_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/multi-agent-collaboration.ipynb": "https://docs.langchain.com/oss/python/langchain/multi-agent",
"how-tos/create-react-agent-manage-message-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"how-tos/many-tools.ipynb": "https://docs.langchain.com/oss/python/langchain/tools",
"tutorials/customer-support/customer-support.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"how-tos/react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
"tutorials/code_assistant/langgraph_code_assistant.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/hierarchical_agent_teams.ipynb": "https://docs.langchain.com/oss/python/langchain/supervisor",
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
"how-tos/human_in_the_loop/wait-user-input.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
@@ -219,12 +233,15 @@ REDIRECT_MAP = {
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"tutorials/plan-and-execute/plan-and-execute.ipynb": "https://docs.langchain.com/oss/python/langchain/middleware/built-in#to-do-list",
"tutorials/langgraph-platform/local-server/local-server.md": "https://docs.langchain.com/langsmith/local-server",
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/react-agent-from-scratch.ipynb": "https://docs.langchain.com/oss/python/langchain/quickstart",
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
@@ -282,8 +299,8 @@ REDIRECT_MAP = {
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/langsmith/deployment/sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/langsmith/deployment/remote_graph/",
# additional exclude-search entries from mkdocs.yml
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
@@ -793,6 +810,17 @@ def on_post_build(config):
# Track which paths have explicit redirects
redirected_paths = set()
# Collect all existing HTML files in the site
all_html_files = set()
for root, dirs, files in os.walk(site_dir):
for file in files:
if file.endswith(".html"):
# Get relative path from site_dir
html_path = os.path.relpath(os.path.join(root, file), site_dir)
# Normalize path separators to forward slashes
html_path = html_path.replace(os.sep, "/")
all_html_files.add(html_path)
# Process explicit redirects from REDIRECT_MAP
for page_old, page_new in REDIRECT_MAP.items():
# Convert .ipynb to .md for path calculation
@@ -847,6 +875,24 @@ def on_post_build(config):
_write_html(site_dir, old_html_path, new_html_path)
# Create catch-all redirects for any HTML files not explicitly redirected
catchall_url = "https://docs.langchain.com/oss/python/langgraph/overview"
for html_file in all_html_files:
# Skip if this file is already explicitly redirected
if html_file in redirected_paths:
continue
# Skip the root index.html (we handle that separately)
if html_file == "index.html":
continue
# Skip reference documentation (keep those accessible)
if html_file.startswith("reference/"):
continue
# Create redirect for this unmapped file
_write_html(site_dir, html_file, catchall_url)
# Create root index.html redirect
root_redirect_html = """<!doctype html>
<html lang="en">
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -97,7 +97,7 @@ class PostgresSaver(BasePostgresSaver):
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
if self.pipe:
self.pipe.sync()
@@ -102,7 +102,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
await cur.execute(
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
)
if self.pipe:
await self.pipe.sync()
@@ -252,7 +252,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
if self.pipe:
self.pipe.sync()
@@ -614,7 +614,9 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
await cur.execute(
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
)
if self.pipe:
await self.pipe.sync()
@@ -91,7 +91,12 @@ WHERE expires_at IS NOT NULL;
VECTOR_MIGRATIONS: Sequence[Migration] = [
Migration(
"""
CREATE EXTENSION IF NOT EXISTS vector;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION vector;
END IF;
END $$;
""",
),
Migration(
+4 -1
View File
@@ -19,7 +19,10 @@ dependencies = [
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+4 -1
View File
@@ -18,7 +18,10 @@ dependencies = [
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
+4 -1
View File
@@ -17,7 +17,10 @@ dependencies = [
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
+4 -1
View File
@@ -25,7 +25,10 @@ inmem = [
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/cli"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[project.scripts]
langgraph = "langgraph_cli.cli:cli"
@@ -1,16 +0,0 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(python:*)",
"Bash(grep:*)",
"Bash(sed:*)",
"Bash(awk:*)",
"Bash(uv run mypy:*)",
"Bash(uv run:*)",
"Bash(make test:*)",
"Bash(make test_parallel:*)"
],
"deny": []
}
}
+4 -4
View File
@@ -151,13 +151,13 @@ def task(
@task
def add_one(a: int) -> int:
def add_one_task(a: int) -> int:
return a + 1
@entrypoint()
def add_one(numbers: list[int]) -> list[int]:
futures = [add_one(n) for n in numbers]
futures = [add_one_task(n) for n in numbers]
results = [f.result() for f in futures]
return results
@@ -173,13 +173,13 @@ def task(
@task
async def add_one(a: int) -> int:
async def add_one_task(a: int) -> int:
return a + 1
@entrypoint()
async def add_one(numbers: list[int]) -> list[int]:
futures = [add_one(n) for n in numbers]
futures = [add_one_task(n) for n in numbers]
return asyncio.gather(*futures)
@@ -30,6 +30,7 @@ __all__ = (
"add_messages",
"MessagesState",
"MessageGraph",
"REMOVE_ALL_MESSAGES",
)
Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
+19 -11
View File
@@ -110,12 +110,20 @@ def _get_node_name(node: StateNode[Any, ContextT]) -> str:
class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
"""A graph whose nodes communicate by reading and writing to a shared state.
The signature of each node is State -> Partial<State>.
The signature of each node is `State -> Partial<State>`.
Each state key can optionally be annotated with a reducer function that
will be used to aggregate the values of that key received from multiple nodes.
The signature of a reducer function is `(Value, Value) -> Value`.
!!! warning
`StateGraph` is a builder class and cannot be used directly for execution.
You must first call `.compile()` to create an executable graph that supports
methods like `invoke()`, `stream()`, `astream()`, and `ainvoke()`. See the
`CompiledStateGraph` documentation for more details.
Args:
state_schema: The schema class that defines the state.
context_schema: The schema class that defines the runtime context.
@@ -289,7 +297,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is inferred as the state schema.
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema.
Will take the name of the function/runnable as the node name.
"""
...
@@ -307,7 +315,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is specified.
"""Add a new node to the `StateGraph`, input schema is specified.
Will take the name of the function/runnable as the node name.
"""
...
@@ -326,7 +334,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is inferred as the state schema."""
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema."""
...
@overload
@@ -343,7 +351,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is specified."""
"""Add a new node to the `StateGraph`, input schema is specified."""
...
def add_node(
@@ -359,7 +367,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph.
"""Add a new node to the `StateGraph`.
Args:
node: The function or runnable this node will run.
@@ -416,7 +424,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
```
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if (retry := kwargs.get("retry", MISSING)) is not MISSING:
warnings.warn(
@@ -571,7 +579,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ValueError: If the start key is `'END'` or if the start key or end key is not present in the graph.
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if self.compiled:
logger.warning(
@@ -676,7 +684,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ValueError: If the sequence contains duplicate node names.
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if len(nodes) < 1:
raise ValueError("Sequence requires at least one node.")
@@ -809,7 +817,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
debug: bool = False,
name: str | None = None,
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
"""Compiles the state graph into a `CompiledStateGraph` object.
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
The compiled graph implements the `Runnable` interface and can be invoked,
streamed, batched, and run asynchronously.
@@ -826,7 +834,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
name: The name to use for the compiled graph.
Returns:
CompiledStateGraph: The compiled state graph.
CompiledStateGraph: The compiled `StateGraph`.
"""
# assign default values
interrupt_before = interrupt_before or []
+352 -250
View File
@@ -258,9 +258,11 @@ def apply_writes(
next_version = None
else:
next_version = get_next_version(
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None,
(
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None
),
None,
)
@@ -491,6 +493,11 @@ def prepare_next_tasks(
PUSH_TRIGGER = (PUSH,)
class _TaskIDFn(Protocol):
def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
pass
def prepare_single_task(
task_path: tuple[Any, ...],
task_id_checksum: str | None,
@@ -520,250 +527,50 @@ def prepare_single_task(
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
if task_path[0] == PUSH and isinstance(task_path[-1], Call):
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_path_t = cast(tuple[str, tuple, int, str, Call], task_path)
call = task_path_t[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
return prepare_push_task_functional(
cast(tuple[str, tuple, int, str, Call], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
)
elif task_path[0] == PUSH:
return prepare_push_task_send(
cast(tuple[str, tuple], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
processes=processes,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
)
else:
return PregelTask(task_id, name, task_path)
elif task_path[0] == PUSH:
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(
f"Ignoring unknown node name {packet.node} in pending sends"
)
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, packet.node, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, task_path)
elif task_path[0] == PULL:
# (PULL, node name)
name = cast(str, task_path[1])
@@ -834,7 +641,7 @@ def prepare_single_task(
if node := proc.node:
if proc.metadata:
metadata.update(proc.metadata)
writes = deque()
writes: deque[tuple[str, Any]] = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(val)
@@ -845,9 +652,11 @@ def prepare_single_task(
name,
),
xxh3_128_hexdigest(
args_key.encode()
if isinstance(args_key, str)
else args_key,
(
args_key.encode()
if isinstance(args_key, str)
else args_key
),
),
cache_policy.ttl,
)
@@ -870,7 +679,9 @@ def prepare_single_task(
node,
writes,
patch_config(
merge_configs(config, additional_config),
merge_configs(
config, cast(RunnableConfig, additional_config)
),
run_name=name,
callbacks=(
manager.get_child(f"graph:step:{step}")
@@ -919,6 +730,297 @@ def prepare_single_task(
return PregelTask(task_id, name, task_path[:3])
def prepare_push_task_functional(
task_path: tuple[str, tuple, int, str, Call],
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
# namespace: bytes, *parts: str | bytes
task_id_func: _TaskIDFn,
) -> PregelTask | PregelExecutableTask:
"""Prepare a push task with an attached caller. Used for the functional API."""
configurable = config.get(CONF, {})
call = task_path[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
in_progress_task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": in_progress_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
configurable.get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
configurable.get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(in_progress_task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
in_progress_task_path,
)
else:
return PregelTask(task_id, name, in_progress_task_path)
def prepare_push_task_send(
task_path: tuple[str, tuple],
# (PUSH, parent task path)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
task_id_func: _TaskIDFn,
processes: Mapping[str, PregelNode],
) -> PregelTask | PregelExecutableTask | None:
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
configurable = config.get(CONF, {})
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
translated_task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": translated_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes: deque[tuple[str, Any]] = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(
translated_task_path, packet.node, writes, triggers
),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
translated_task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, translated_task_path)
def checkpoint_null_version(
checkpoint: Checkpoint,
) -> V | None:
-3
View File
@@ -459,9 +459,6 @@ class PregelLoop:
def tick(self) -> bool:
"""Execute a single iteration of the Pregel loop.
Args:
input_keys: The key(s) to read input from.
Returns:
True if more iterations are needed.
"""
+18 -16
View File
@@ -2354,7 +2354,7 @@ class Pregel(
validate_keys(output_keys, self.channels)
interrupt_before = interrupt_before or self.interrupt_before_nodes
interrupt_after = interrupt_after or self.interrupt_after_nodes
if not isinstance(stream_mode, list):
if isinstance(stream_mode, str):
stream_modes = {stream_mode}
else:
stream_modes = set(stream_mode)
@@ -2431,11 +2431,12 @@ class Pregel(
Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
The streamed outputs will be tuples of `(mode, data)`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
output_keys: The keys to stream, defaults to all non-context channels.
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
@@ -2452,7 +2453,7 @@ class Pregel(
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
Yields:
The output of each step in the graph. The output shape depends on the `stream_mode`.
@@ -2697,12 +2698,14 @@ class Pregel(
- `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
The streamed outputs will be tuples of `(mode, data)`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
output_keys: The keys to stream, defaults to all non-context channels.
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
@@ -2719,7 +2722,7 @@ class Pregel(
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
Yields:
The output of each step in the graph. The output shape depends on the `stream_mode`.
@@ -3101,31 +3104,30 @@ class Pregel(
durability: Durability | None = None,
**kwargs: Any,
) -> dict[str, Any] | Any:
"""Asynchronously invoke the graph on a single input.
"""Asynchronously run the graph with a single input and config.
Args:
input: The input data for the computation. It can be a dictionary or any other type.
config: The configuration for the computation.
input: The input data for the graph. It can be a dictionary or any other type.
config: The configuration for the graph run.
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The stream mode for the computation.
stream_mode: The stream mode for the graph run.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
output_keys: The output keys to include in the result.
interrupt_before: The nodes to interrupt before.
interrupt_after: The nodes to interrupt after.
output_keys: The output keys to retrieve from the graph run.
interrupt_before: The nodes to interrupt the graph run before.
interrupt_after: The nodes to interrupt the graph run after.
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
**kwargs: Additional keyword arguments.
**kwargs: Additional keyword arguments to pass to the graph run.
Returns:
The result of the computation. If `stream_mode` is `"values"`, it returns the latest value.
If `stream_mode` is `"chunks"`, it returns a list of chunks.
The output of the graph run. If `stream_mode` is `"values"`, it returns the latest output.
If `stream_mode` is not `"values"`, it returns a list of output chunks.
"""
output_keys = output_keys if output_keys is not None else self.output_channels
latest: dict[str, Any] | Any = None
+1 -1
View File
@@ -111,7 +111,7 @@ class Runtime(Generic[ContextT]):
stream_writer=other.stream_writer
if other.stream_writer is not _no_op_stream_writer
else self.stream_writer,
previous=other.previous or self.previous,
previous=self.previous if other.previous is None else other.previous,
)
def override(
+2 -2
View File
@@ -31,13 +31,13 @@ ContextT_contra = TypeVar(
)
InputT = TypeVar("InputT", bound=StateLike, default=StateT)
"""Type variable used to represent the input to a state graph.
"""Type variable used to represent the input to a `StateGraph`.
Defaults to `StateT`.
"""
OutputT = TypeVar("OutputT", bound=StateLike, default=StateT)
"""Type variable used to represent the output of a state graph.
"""Type variable used to represent the output of a `StateGraph`.
Defaults to `StateT`.
"""
+9 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.0.2"
version = "1.0.3"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
@@ -32,8 +32,15 @@ dependencies = [
"pydantic>=2.7.4",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Homepage = "https://docs.langchain.com/oss/python/langgraph/overview"
Documentation = "https://reference.langchain.com/python/langgraph/"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/langgraph"
Changelog = "https://github.com/langchain-ai/langgraph/releases"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
+3 -3
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.14'",
@@ -1345,7 +1345,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.2"
version = "1.0.3"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -1710,7 +1710,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "1.0.2"
version = "1.0.4"
source = { editable = "../prebuilt" }
dependencies = [
{ name = "langchain-core" },
+21
View File
@@ -0,0 +1,21 @@
{
"permissions": {
"allow": [
"Bash(make test:*)",
"Bash(uv run pytest:*)",
"Bash(LANGGRAPH_TEST_FAST=0 make start-services:*)",
"Bash(LANGGRAPH_TEST_FAST=0 uv run:*)",
"Bash(EXIT_CODE=$?)",
"Bash(make stop-services:*)",
"Bash(exit $EXIT_CODE)",
"Read(//Users/sydney_runkle/oss/langgraph/**)",
"Bash(python3:*)",
"Bash(find:*)",
"Bash(python -m pytest:*)",
"Bash(python:*)",
"Read(//tmp/**)"
],
"deny": [],
"ask": []
}
}
@@ -63,7 +63,7 @@ class AgentState(TypedDict):
@deprecated(
"AgentStatePydantic has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStatePydantic`.",
"AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStatePydantic(BaseModel):
@@ -78,11 +78,11 @@ with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
category=LangGraphDeprecatedSinceV10,
message="AgentState has been moved to langchain.agents.*",
message="AgentState has been moved to `langchain.agents`.*",
)
@deprecated(
"AgentStateWithStructuredResponse has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStateWithStructuredResponse`.",
"AgentStateWithStructuredResponse has been deprecated in favor of AgentState in `langchain.agents`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStateWithStructuredResponse(AgentState):
@@ -95,11 +95,11 @@ with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
category=LangGraphDeprecatedSinceV10,
message="AgentStatePydantic has been moved to langchain.agents.*",
message="AgentStatePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
)
@deprecated(
"AgentStateWithStructuredResponsePydantic has been moved to `langchain.agents`. Please update your import to `from langchain.agents import AgentStateWithStructuredResponsePydantic`.",
"AgentStateWithStructuredResponsePydantic has been deprecated in favor of AgentState in `langchain.agents`.",
category=LangGraphDeprecatedSinceV10,
)
class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):
+22 -2
View File
@@ -142,6 +142,25 @@ class ToolCallRequest:
state: Any
runtime: ToolRuntime
def __setattr__(self, name: str, value: Any) -> None:
"""Raise deprecation warning when setting attributes directly.
Direct attribute assignment is deprecated. Use the `override()` method instead.
"""
import warnings
# Allow setting attributes during initialization
if not hasattr(self, "__dataclass_fields__") or not hasattr(self, name):
object.__setattr__(self, name, value)
else:
warnings.warn(
f"Setting attribute '{name}' on ToolCallRequest is deprecated. "
"Use the override() method instead to create a new instance with modified values.",
DeprecationWarning,
stacklevel=2,
)
object.__setattr__(self, name, value)
def override(
self, **overrides: Unpack[_ToolCallRequestOverrides]
) -> ToolCallRequest:
@@ -202,8 +221,9 @@ Examples:
```python
def handler(request, execute):
request.tool_call["args"]["value"] *= 2
return execute(request)
modified_call = {**request.tool_call, "args": {**request.tool_call["args"], "value": request.tool_call["args"]["value"] * 2}}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
```
Retry on error (execute multiple times):
+5 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-prebuilt"
version = "1.0.2"
version = "1.0.4"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
authors = []
requires-python = ">=3.10"
@@ -29,7 +29,10 @@ dependencies = [
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/prebuilt"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [
+83 -9
View File
@@ -130,11 +130,17 @@ def test_modify_arguments() -> None:
execute: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""Handler that doubles the input arguments."""
# Modify the arguments
request.tool_call["args"]["a"] *= 2
request.tool_call["args"]["b"] *= 2
return execute(request)
# Modify the arguments using override method
modified_call = {
**request.tool_call,
"args": {
**request.tool_call["args"],
"a": request.tool_call["args"]["a"] * 2,
"b": request.tool_call["args"]["b"] * 2,
},
}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
tool_node = ToolNode([add], wrap_tool_call=modify_args_handler)
@@ -362,10 +368,17 @@ async def test_handler_with_async_execution() -> None:
execute: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""Handler that modifies arguments."""
# Add 10 to both arguments
request.tool_call["args"]["a"] += 10
request.tool_call["args"]["b"] += 10
return execute(request)
# Add 10 to both arguments using override method
modified_call = {
**request.tool_call,
"args": {
**request.tool_call["args"],
"a": request.tool_call["args"]["a"] + 10,
"b": request.tool_call["args"]["b"] + 10,
},
}
modified_request = request.override(tool_call=modified_call)
return execute(modified_request)
tool_node = ToolNode([async_add], wrap_tool_call=modifying_handler)
@@ -1305,3 +1318,64 @@ async def test_state_extraction_with_tool_call_with_context_async() -> None:
assert state_seen[0] == actual_state
assert "__type" not in state_seen[0]
assert "tool_call" not in state_seen[0]
def test_tool_call_request_is_frozen() -> None:
"""Test that ToolCallRequest raises deprecation warnings on direct attribute reassignment."""
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
state: dict = {"messages": []}
runtime = None
request = ToolCallRequest(
tool_call=tool_call, tool=add, state=state, runtime=runtime
) # type: ignore[arg-type]
# Test that direct attribute reassignment raises DeprecationWarning
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'tool_call' on ToolCallRequest is deprecated",
):
request.tool_call = {"name": "other", "args": {}, "id": "call_2"} # type: ignore[misc]
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'tool' on ToolCallRequest is deprecated",
):
request.tool = None # type: ignore[misc]
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'state' on ToolCallRequest is deprecated",
):
request.state = {} # type: ignore[misc]
with pytest.warns(
DeprecationWarning,
match="Setting attribute 'runtime' on ToolCallRequest is deprecated",
):
request.runtime = None # type: ignore[misc]
# Test that override method works correctly
new_tool_call: ToolCall = {
"name": "multiply",
"args": {"x": 5, "y": 10},
"id": "call_3",
}
# Original request should be unchanged (note: it was modified by the warnings tests above)
# So we create a fresh request to test override properly
fresh_request = ToolCallRequest(
tool_call=tool_call, tool=add, state=state, runtime=runtime
) # type: ignore[arg-type]
fresh_new_request = fresh_request.override(tool_call=new_tool_call)
# Original request should be unchanged
assert fresh_request.tool_call == tool_call
assert fresh_request.tool_call["name"] == "add"
# New request should have the updated tool_call
assert fresh_new_request.tool_call == new_tool_call
assert fresh_new_request.tool_call["name"] == "multiply"
assert fresh_new_request.tool == add # Other fields should remain the same
assert fresh_new_request.state == state
assert fresh_new_request.runtime is None
+62
View File
@@ -1610,3 +1610,65 @@ def test_tool_node_stream_writer() -> None:
},
),
]
def test_tool_call_request_setattr_deprecation_warning():
"""Test that ToolCallRequest raises a deprecation warning on direct attribute modification."""
import warnings
from langgraph.prebuilt.tool_node import ToolCallRequest
# Create a mock ToolCall
tool_call = {"name": "test", "args": {"a": 1}, "id": "call_1", "type": "tool_call"}
# Create a ToolCallRequest
request = ToolCallRequest(
tool_call=tool_call,
tool=None,
state={"messages": []},
runtime=None,
)
# Test 1: Direct attribute assignment should raise deprecation warning but still work
with pytest.warns(DeprecationWarning, match="deprecated.*override"):
request.tool_call = {"name": "other", "args": {}, "id": "call_2"}
# Verify the attribute was actually modified
assert request.tool_call == {"name": "other", "args": {}, "id": "call_2"}
# Reset for further tests
with warnings.catch_warnings():
warnings.simplefilter("ignore")
request.tool_call = tool_call
# Test 2: override method should work without warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
new_tool_call = {
"name": "new_tool",
"args": {"b": 2},
"id": "call_3",
"type": "tool_call",
}
new_request = request.override(tool_call=new_tool_call)
# Verify no warning was raised
assert len(w) == 0
# Verify original is unchanged
assert request.tool_call == tool_call
# Verify new request has updated values
assert new_request.tool_call == new_tool_call
# Test 3: Initialization should not trigger warning
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
ToolCallRequest(
tool_call=tool_call,
tool=None,
state={"messages": []},
runtime=None,
)
# Verify no warning was raised during initialization
assert len(w) == 0
+3 -3
View File
@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10"
[[package]]
@@ -246,7 +246,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.2"
version = "1.0.3"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -467,7 +467,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "1.0.2"
version = "1.0.4"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -259,7 +259,7 @@ class Auth:
"""
if self._authenticate_handler is not None:
raise ValueError(
"Authentication handler already set as {self._authenticate_handler}."
f"Authentication handler already set as {self._authenticate_handler}."
)
self._authenticate_handler = fn
return fn
+6 -6
View File
@@ -1,11 +1,11 @@
"""The LangGraph client implementations connect to the LangGraph API.
This module provides both asynchronous ([get_client(url="http://localhost:2024"))](#get_client) or [LangGraphClient](#LangGraphClient))
and synchronous ([get_sync_client(url="http://localhost:2024"))](#get_sync_client) or [SyncLanggraphClient](#SyncLanggraphClient))
clients to interacting with the LangGraph API's core resources such as
Assistants, Threads, Runs, and Cron jobs, as well as its persistent
document Store.
""" # noqa: E501
This module provides both asynchronous (`get_client(url="http://localhost:2024")` or
`LangGraphClient`) and synchronous (`get_sync_client(url="http://localhost:2024")` or
`SyncLanggraphClient`) clients to interacting with the LangGraph API's core resources
such as Assistants, Threads, Runs, and Cron jobs, as well as its persistent document
Store.
"""
from __future__ import annotations
+4 -1
View File
@@ -20,7 +20,10 @@ dependencies = [
path = "langgraph_sdk/__init__.py"
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-py"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
test = [