Compare commits

..
Author SHA1 Message Date
Sydney Runkle f1f4d8387f Merge branch 'sr/perms-for-pr' of https://github.com/langchain-ai/langgraph into sr/perms-for-pr 2025-07-02 11:05:19 -04:00
Sydney Runkle fab56ea75a added token 2025-07-02 11:05:13 -04:00
Sydney RunkleandGitHub 0af0462628 Merge branch 'main' into sr/perms-for-pr 2025-07-02 11:00:21 -04:00
Sydney Runkle 532340e6cb use new token 2025-07-02 10:39:41 -04:00
Sydney RunkleandGitHub 000f5c3043 fix[deps]: update lockfiles / deps bounds for internal tools (#5301)
update lockfiles / deps bounds
2025-07-02 10:30:55 -04:00
Sydney RunkleandGitHub b3708bd7f6 ci: add automated uv lock --upgrade workflow (#5307) 2025-07-02 10:10:01 -04:00
Sydney RunkleandGitHub 8271e39e00 dependabot: no kafka (#5306)
* fix list of dirs
* another patch
2025-07-02 13:15:00 +00:00
Sydney RunkleandGitHub 60560ea755 dependabot: fix list of dirs for pip updates (#5305)
fix list of dirs
2025-07-02 13:12:22 +00:00
waqarahmed6095andGitHub e2acfb24cc Update use_stream_react.md (#5304)
Problem of two times heading 
"How to integrate LangGraph into your React application"
2025-07-02 13:10:57 +00:00
Sydney RunkleandGitHub 191192b142 upgrade dependabot scope (#5303) 2025-07-02 09:09:11 -04:00
Josh RogersandGitHub df368bdd30 Updating message types to include all base message fields (#5298) 2025-07-01 15:40:13 -07:00
Josh RogersandGitHub 4ec897033f Update LGP api reference docs (#5297) 2025-07-01 11:45:30 -07:00
c16e42e6d5 fix broken link (#5291)
* fix broken link

* Apply suggestions from code review

Fix link

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>

---------

Co-authored-by: Lauren Hirata Singh <lauren@langchain.dev>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-07-01 13:52:26 +00:00
David DuongandGitHub 376469ea90 release(sdk-js): 0.0.88 (#5294) 2025-07-01 14:30:37 +02:00
Tat Dat Duong 7e2af0ce8d release(sdk-js): 0.0.88 2025-07-01 14:21:11 +02:00
Youssef Ahmed Mohamed AbdelrahmanandGitHub 1b205a99cb docs: fix typo in application_structure (#5289) 2025-07-01 12:09:14 +00:00
Sam CrowderandGitHub 0a8ba20f5f docs: remove beta flag on self hosted plane (#5288) 2025-06-30 22:41:05 -04:00
Sam CrowderandGitHub 048cb3584c self hosted control plane no longer in beta (#5286)
* self hosted control plane no longer in beta

* accidental changes
2025-06-30 17:34:18 -07:00
Sam CrowderandGitHub 22c35b7bc8 switch order of MCP methods in API spec (#5287) 2025-06-30 17:33:59 -07:00
David DuongandGitHub f3ed32e611 feat(react): enhance useStream with initialValues, newThreadId, and onStop callback for improved UX (#5111) 2025-07-01 01:53:28 +02:00
Tat Dat Duong 276675b618 Make sure to spread stream values 2025-07-01 01:39:53 +02:00
Tat Dat Duong 882de42996 Fix non-existent assistantId 2025-07-01 01:35:59 +02:00
Tat Dat Duong 70be50f37b Fix typo 2025-07-01 01:33:08 +02:00
Tat Dat Duong 1c7234e9c5 Update README.md 2025-07-01 01:32:27 +02:00
Tat Dat Duong 3d88f75254 Cleanup 2025-07-01 01:19:07 +02:00
Lauren Hirata SinghandGitHub 407abbe9ff Add forum links (#5282) 2025-06-30 16:11:46 -04:00
ccurmeandGitHub 6182cd1dcb prebuilt: release 0.5.2 (#5280) 2025-06-30 15:50:21 -04:00
Lauren Hirata SinghandGitHub 1d276dd753 docs: cronjob nav (#5281) 2025-06-30 15:34:29 -04:00
ccurmeandGitHub a48d8cb69b prebuilt[patch]: import recognized tool message content block types from langchain-core (#5275) 2025-06-30 15:22:19 -04:00
Lauren Hirata SinghandGitHub a05a251caf docs: Fix nav (#5279) 2025-06-30 15:18:43 -04:00
MauritsBrinkmanandTat Dat Duong c7bbb26ac0 test: add useStream onStop callback tests 2025-06-30 16:43:48 +02:00
MauritsBrinkmanandTat Dat Duong ac9b6c416e feat: add onStop callback to useStream for custom stop behavior
Add onStop callback to useStream hook enabling developers to customize
UI behavior when streams are stopped. This is especially useful for
UI messages with loading states that need to show "stopped" status
instead of remaining in infinite loading state.

The callback provides the same mutate function as onCustomEvent for
immediate local state updates, while users can optionally update
server thread state using the threads client.

Example usage:
```typescript
const stream = useStream({
  assistantId: "my-assistant",
  onStop: async ({ mutate }) => {
    // Immediate UI update - stop loading components
    mutate((prev) => ({
      ...prev,
      ui: prev.ui?.map(component =>
        component.props?.isLoading
          ? {
              ...component,
              props: {
                ...component.props,
                isLoading: false,
                isStopped: true
              }
            }
          : component
      )
    }));

    // Optional server thread state update
    if (stream.threadId) {
      await stream.client.threads.updateState(stream.threadId, {
        values: {
          ui: prev.ui // persist stopped state to server
        }
      });
    }
  }
});
```

This is especially useful for cases where gen UI components have loading states,
where we don't want the loading state to persist on cancellation.
2025-06-30 16:43:13 +02:00
MauritsBrinkmanandTat Dat Duong d4b4eebe4a fix(sdk-js): convert SSE classes to factory functions to resolve tree shaking
- Convert BytesLineDecoder and SSEDecoder from classes extending TransformStream to factory functions
- Fixes tree shaking failures that prevented build completion
- Maintains identical API functionality, just removes 'new' keyword usage
- All tests continue to pass

Resolves tree shaking side effect detection issues with TransformStream extension
2025-06-30 16:43:13 +02:00
MauritsBrinkmanandTat Dat Duong f8e1e803e1 docs(react): add documentation and tests for initialValues and newThreadId options
- Document initialValues for cached thread display
- Document newThreadId for optimistic thread creation
- Add comprehensive test coverage for both features
2025-06-30 16:43:12 +02:00
MauritsBrinkmanandTat Dat Duong 141a6af4f7 feat(react): add initialValues option to useStream for cached thread display
Add initialValues parameter to UseStreamOptions to enable immediate display
of cached thread data while official history is being fetched from the server.

This addresses the common use case where applications cache thread data
locally (IndexedDB, localStorage, etc.) and want to show it instantly when
users navigate to existing threads, providing better UX with faster loading.

Key changes:
- Add initialValues?: Partial<StateType> | null to UseStreamOptions interface
- Update values precedence: streamValues > initialValues > historyValues
- Ensure optimisticValues properly override initialValues during submission
- Maintain full backward compatibility with existing API

Example usage:
```typescript
const stream = useStream({
  threadId,
  assistantId: 'my-assistant',
  initialValues: cachedThreadData?.values // Show cached data immediately
});
```

The values flow now follows this priority:
1. Initial load: shows initialValues while history loads
2. During submit: optimisticValues take precedence
3. After server response: official history replaces all
2025-06-30 16:43:12 +02:00
MauritsBrinkmanandTat Dat Duong 8a763ad358 feat(react): add newThreadId option to useStream for optimistic UI
Add optional newThreadId parameter to useStream hook that allows specifying
a thread ID for new thread creation while keeping threadId null. This enables
optimistic UI patterns where developers need to know the thread ID beforehand
for routing/navigation without causing 404 errors from attempting to fetch
non-existent thread history.

Usage:
- Set threadId: null and newThreadId: "predetermined-id"
- Submit message to create thread with specified ID
- Use onThreadId callback to update threadId after creation

This solves the UX problem of having to await thread creation before
enabling optimistic navigation to e.g. /[threadId] routes.
2025-06-30 16:43:12 +02:00
David DuongandGitHub 16d02e63a1 fix: Allow configuring stream mode in useStream.joinStream() (#5146) 2025-06-30 16:37:56 +02:00
David DuongandGitHub 03421c2b04 chore(sdk-js): use embed LGP server for MSW mocking (#5174) 2025-06-30 16:33:47 +02:00
Tat Dat Duong 2508aa45ea Use published package 2025-06-30 13:51:32 +02:00
Sam CrowderandGitHub 9e035264f8 slightly more explanation when we say dont use in serverless (#5245)
slightly more explanation
2025-06-29 21:59:48 -04:00
joaquin-borggio-lcandGitHub 84e14f47bf docs: Add pre-req for egress to control plane (#5241)
added pre-req for egress
2025-06-27 14:18:39 -07:00
Lauren Hirata SinghandGitHub 83bbe42eab docs: Nav reorg (#5236)
* docs: Nav consolidation

* nav

* reorg

* fix links

* fix

* prebuilts

* fix spelling

* reorg

* reorg
2025-06-27 14:06:26 -04:00
David DuongandGitHub 70894af72c fix(sdk-js): avoid stale client when fetching history (#5240) 2025-06-27 20:04:41 +02:00
Tat Dat Duong e466327524 Bump to 0.0.87 2025-06-27 20:03:03 +02:00
Tat Dat Duong b6655fe083 Use the client hash only in useEffect 2025-06-27 19:59:21 +02:00
Tat Dat Duong 9b7cc1c82e fix(sdk-js): avoid stale client when fetching history 2025-06-27 19:51:49 +02:00
Nuno CamposandGitHub 80d6bddd1b Fix deadlock in SqliteStore (#5234) 2025-06-27 09:03:57 -07:00
Nuno Campos c406aede96 Fix deadlock in SqliteStore
- If setup wasnt called separately _cursor() and setup() would deadlock
- The call to setup() in _cursor() should be outside the lock block, as setup() also acquires the lock and re-checks the setup flag
2025-06-27 08:57:35 -07:00
Eugene YurtsevandGitHub 96bfc8bad9 docs: cross links for functional api (#5231)
add cross-links
2025-06-27 11:31:18 -04:00
Tat Dat Duong 9a0cee5cd0 chore(sdk-js): use embed LGP server for MSW mocking 2025-06-24 02:14:19 +02:00
bracesproul 78bef6bc0c formatting 2025-06-19 11:20:17 -07:00
bracesproul 7bd364c457 fix: Allow configuring stream mode in useStream.joinStream() 2025-06-19 11:14:59 -07:00
47 changed files with 3433 additions and 1264 deletions
+3 -3
View File
@@ -10,6 +10,6 @@ contact_links:
- name: Show and tell
about: Show what you built with LangChain
url: https://github.com/langchain-ai/langgraph/discussions/categories/show-and-tell
- name: Slack
url: https://www.langchain.com/join-community
about: General community discussions
- name: LangChain Forum
url: https://forum.langchain.com/
about: General community discussions and support
+12 -5
View File
@@ -1,11 +1,18 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# and
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directories:
- "libs/checkpoint"
- "libs/checkpoint-postgres"
- "libs/checkpoint-sqlite"
- "libs/cli"
- "libs/langgraph"
- "libs/prebuilt"
- "libs/sdk-py"
schedule:
interval: "weekly"
+8 -4
View File
@@ -22,6 +22,7 @@ jobs:
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
@@ -38,6 +39,9 @@ jobs:
- 'libs/prebuilt/**'
sdk-js:
- 'libs/sdk-js/**'
deps:
- '**/pyproject.toml'
- '**/uv.lock'
lint:
needs: changes
@@ -55,7 +59,7 @@ jobs:
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -74,7 +78,7 @@ jobs:
"libs/checkpoint-postgres",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -83,7 +87,7 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
@@ -140,7 +144,7 @@ jobs:
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.deps == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
+49
View File
@@ -0,0 +1,49 @@
name: lockfile upgrades
on:
schedule:
# run at midnight every Sunday
- cron: '0 0 * * 0'
# allow manual triggering
workflow_dispatch:
pull_request:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
upgrade-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
# use minimum supported Python version
python-version: "3.9"
enable-cache: true
cache-suffix: "uv-lock-upgrade"
- name: Run uv lock --upgrade in all Python packages
run: make lock-upgrade
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.LANGGRAPH_WRITE_TOKEN }}
commit-message: "chore: upgrade dependencies with `uv lock --upgrade`"
title: "chore: upgrade dependencies with `uv lock --upgrade`"
body: |
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
This is an automated PR created by the "lockfile upgrades" workflow.
branch: deps/uv-lock-upgrade
base: main
delete-branch: true
labels: |
dependencies
+10
View File
@@ -47,6 +47,16 @@ lock:
fi; \
done
# Lock all projects and upgrade dependencies
.PHONY: lock-upgrade
lock-upgrade:
@for dir in $(LIBS_DIRS); do \
if [ -f $$dir/Makefile ]; then \
echo "Running lock-upgrade in $$dir"; \
(cd $$dir && uv lock --upgrade); \
fi; \
done
# Test all projects
.PHONY: test
test:
+1 -1
View File
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
+1
View File
@@ -111,6 +111,7 @@ REDIRECT_MAP = {
"concepts/v0-human-in-the-loop.md": "concepts/human-in-the-loop.md",
"how-tos/index.md": "index.md",
"tutorials/introduction.ipynb": "concepts/why-langgraph.md",
"agents/deployment.md": "tutorials/langgraph-platform/local-server.md",
# deployment redirects
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
-92
View File
@@ -1,92 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Deployment
To deploy your LangGraph agent, create and configure a LangGraph app. This setup supports both local development and production deployments.
Features:
* 🖥️ Local server for development
* 🧩 Studio Web UI for visual debugging
* ☁️ Cloud and 🔧 self-hosted deployment options
* 📊 LangSmith integration for tracing and observability
!!! info "Requirements"
- ✅ You **must** have a [LangSmith account](https://www.langchain.com/langsmith). You can sign up for **free** and get started with the free tier.
## Create a LangGraph app
```bash
pip install -U "langgraph-cli[inmem]"
langgraph new path/to/your/app --template new-langgraph-project-python
```
This will create an empty LangGraph project. You can modify it by replacing the code in `src/agent/graph.py` with your agent code. For example:
```python
from langgraph.prebuilt import create_react_agent
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
graph = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
prompt="You are a helpful assistant"
)
```
### Install dependencies
In the root of your new LangGraph app, install the dependencies in `edit` mode so your local changes are used by the server:
```shell
pip install -e .
```
### Create an `.env` file
You will find a `.env.example` in the root of your new LangGraph app. Create
a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys:
```bash
LANGSMITH_API_KEY=lsv2...
ANTHROPIC_API_KEY=sk-
```
## Launch LangGraph server locally
```shell
langgraph dev
```
This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
> Ready!
>
> - API: [http://localhost:2024](http://localhost:2024/)
>
> - Docs: http://localhost:2024/docs
>
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
See this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/) to learn more about running LangGraph app locally.
## LangGraph Studio Web UI
LangGraph Studio Web is a specialized UI that you can connect to LangGraph API server to enable visualization, interaction, and debugging of your application locally. Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph dev` command.
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
## Deployment
Once your LangGraph app is running locally, you can deploy it using LangGraph Platform. Refer to the [deployment options guide](../concepts/deployment_options.md) for detailed instructions on all supported deployment models.
+2 -2
View File
@@ -8,9 +8,9 @@ hide:
- tags
---
# Agent development with LangGraph
# Agent development using prebuilt components
**LangGraph** provides both low-level primitives and high-level prebuilt components for building agent-based applications. This section focuses on the **prebuilt**, **reusable** components designed to help you construct agentic systems quickly and reliably—without the need to implement orchestration, memory, or human feedback handling from scratch.
LangGraph provides both low-level primitives and high-level prebuilt components for building agent-based applications. This section focuses on the prebuilt, ready-to-use components designed to help you construct agentic systems quickly and reliably—without the need to implement orchestration, memory, or human feedback handling from scratch.
## What is an agent?
@@ -3,7 +3,7 @@
Before deploying, review the [conceptual guide for the Self-Hosted Control Plane](../../concepts/langgraph_self_hosted_control_plane.md) deployment option.
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](../../concepts/plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](../../concepts/plans.md) plan.
## Prerequisites
@@ -3,7 +3,7 @@
Before deploying, review the [conceptual guide for the Self-Hosted Data Plane](../../concepts/langgraph_self_hosted_data_plane.md) deployment option.
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](../../concepts/plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](../../concepts/plans.md) plan.
## Prerequisites
@@ -15,11 +15,15 @@ Before deploying, review the [conceptual guide for the Self-Hosted Data Plane](.
### Prerequisites
1. `KEDA` is installed on your cluster.
helm repo add kedacore https://kedacore.github.io/charts
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace
1. A valid `Ingress` controller is installed on your cluster.
1. You have slack space in your cluster for multiple deployments. `Cluster-Autoscaler` is recommended to automatically provision new nodes.
1. You will need to enable egress to two control plane URLs. The listener polls these endpoints for deployments:
https://api.host.langchain.com
https://api.smith.langchain.com
### Setup
+69 -1
View File
@@ -1,4 +1,4 @@
How to integrate LangGraph into your React application# How to integrate LangGraph into your React application
# How to integrate LangGraph into your React application
!!! info "Prerequisites"
@@ -503,6 +503,74 @@ const handleSubmit = (text: string) => {
};
```
### Cached Thread Display
Use the `initialValues` option to display cached thread data immediately while the history is being loaded from the server. This improves user experience by showing cached data instantly when navigating to existing threads.
```tsx
import { useStream } from "@langchain/langgraph-sdk/react";
const CachedThreadExample = ({ threadId, cachedThreadData }) => {
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId,
// Show cached data immediately while history loads
initialValues: cachedThreadData?.values,
messagesKey: "messages",
});
return (
<div>
{stream.messages.map((message) => (
<div key={message.id}>{message.content as string}</div>
))}
</div>
);
};
```
### Optimistic Thread Creation
Use the `threadId` option in `submit` function to enable optimistic UI patterns where you need to know the thread ID before the thread is actually created.
```tsx
import { useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";
const OptimisticThreadExample = () => {
const [threadId, setThreadId] = useState<string | null>(null);
const [optimisticThreadId] = useState(() => crypto.randomUUID());
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "agent",
threadId,
onThreadId: setThreadId, // (3) Updated after thread has been created.
messagesKey: "messages",
});
const handleSubmit = (text: string) => {
// (1) Perform a soft navigation to /threads/${optimisticThreadId}
// without waiting for thread creation.
window.history.pushState({}, "", `/threads/${optimisticThreadId}`);
// (2) Submit message to create thread with the predetermined ID.
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ threadId: optimisticThreadId }
);
};
return (
<div>
<p>Thread ID: {threadId ?? optimisticThreadId}</p>
{/* Rest of component */}
</div>
);
};
```
### TypeScript
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -395,7 +395,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
=== "Python"
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform closed beta. Requires a license key for production use.
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform. Requires a license key for production use.
**Usage**
@@ -422,7 +422,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
=== "JS"
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform closed beta. Requires a license key for production use.
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Platform. Requires a license key for production use.
**Usage**
+2 -2
View File
@@ -48,7 +48,7 @@ Below are examples of directory structures for Python and JavaScript application
│ ├── utils # utilities for your graph
│ │ ├── __init__.py
│ │ ├── tools.py # tools for your graph
│ │ ├── nodes.py # node functions for you graph
│ │ ├── nodes.py # node functions for your graph
│ │ └── state.py # state definition of your graph
│ ├── __init__.py
│ └── agent.py # code for constructing your graph
@@ -64,7 +64,7 @@ Below are examples of directory structures for Python and JavaScript application
├── src # all project code lies within here
│ ├── utils # optional utilities for your graph
│ │ ├── tools.ts # tools for your graph
│ │ ├── nodes.ts # node functions for you graph
│ │ ├── nodes.ts # node functions for your graph
│ │ └── state.ts # state definition of your graph
│ └── agent.ts # code for constructing your graph
├── package.json # package dependencies
+4 -4
View File
@@ -18,9 +18,9 @@ There are 4 main options for deploying with the [LangGraph Platform](langgraph_p
1. [Cloud SaaS](#cloud-saas)
1. [Self-Hosted Data Plane<sup>(Beta)</sup>](#self-hosted-data-plane)
1. [Self-Hosted Data Plane](#self-hosted-data-plane)
1. [Self-Hosted Control Plane<sup>(Beta)</sup>](#self-hosted-control-plane)
1. [Self-Hosted Control Plane](#self-hosted-control-plane)
1. [Standalone Container](#standalone-container)
@@ -50,7 +50,7 @@ For more information, please see:
## Self-Hosted Data Plane
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](../concepts/plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](../concepts/plans.md) plan.
The [Self-Hosted Data Plane](./langgraph_self_hosted_data_plane.md) deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us.
@@ -66,7 +66,7 @@ For more information, please see:
## Self-Hosted Control Plane
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](../concepts/plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](../concepts/plans.md) plan.
The [Self-Hosted Control Plane](./langgraph_self_hosted_control_plane.md) deployment option is a fully self-hosted model for deployment where you manage the [control plane](./langgraph_control_plane.md) and [data plane](./langgraph_data_plane.md) in your cloud. This option gives you full control and responsibility of the control plane and data plane infrastructure.
+1 -1
View File
@@ -47,7 +47,7 @@ LangGraph is a stateful, orchestration framework that brings added control to ag
No. LangGraph Platform is proprietary software.
There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option is free while in beta, but will eventually be a paid service. We will always give ample notice before charging for a service and reward our early adopters with preferential pricing. The Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more.
There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option and the Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more.
For more information, see our [LangGraph Platform pricing page](https://www.langchain.com/pricing-langgraph-platform).
+14 -12
View File
@@ -18,10 +18,21 @@ The Functional API uses two key building blocks:
This provides a minimal abstraction for building workflows with state management and streaming.
!!! tip
!!! tip
For information on how to use the functional API, see [Use Functional API](../how-tos/use-functional-api.md).
## Functional API vs. Graph API
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms.
## Example
@@ -532,15 +543,6 @@ While different runs of a workflow can produce different results, resuming a **s
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
## Functional API vs. Graph API
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences:
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
## Common Pitfalls
### Handling side effects
@@ -3,7 +3,7 @@
There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane).
!!! info "Important"
The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan.
The Self-Hosted Control Plane deployment option requires an [Enterprise](plans.md) plan.
## Requirements
@@ -8,7 +8,7 @@ search:
There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane).
!!! info "Important"
The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan.
The Self-Hosted Data Plane deployment option requires an [Enterprise](plans.md) plan.
## Requirements
@@ -19,7 +19,7 @@ The Standalone Container deployment option is the least restrictive model for de
!!! warning
LangGraph Platform should not be deployed in serverless environments.
LangGraph Platform should not be deployed in serverless environments. Scale to zero may cause task loss and scaling up will not work reliably.
## Architecture
+8 -1
View File
@@ -1,5 +1,12 @@
# Use the functional API
The [**Functional API**](../concepts/functional_api.md) allows you to add LangGraph's key features — [persistence](../concepts/persistence.md), [memory](../how-tos/memory/add-memory.md), [human-in-the-loop](../concepts/human_in_the_loop.md), and [streaming](../concepts/streaming.md) — to your applications with minimal changes to your existing code.
!!! tip
For conceptual information on the functional API, see [Functional API](../concepts/functional_api.md).
## Creating a simple workflow
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
@@ -832,4 +839,4 @@ for chunk in workflow.stream([input_message], config, stream_mode="values"):
## Integrate with other libraries
* [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box.
* [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box.
@@ -1,4 +1,4 @@
# LangGraph Platform quickstart
# Run a local server
This guide shows you how to run a LangGraph application locally.
+125 -113
View File
@@ -92,38 +92,75 @@ nav:
- Get started:
- index.md
- Quickstarts:
- Agent: agents/agents.md
- LangGraph basics:
- Start with a prebuilt agent: agents/agents.md
- Build a custom workflow:
- concepts/why-langgraph.md
- Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
- tutorials/get-started/2-add-tools.md
- tutorials/get-started/3-add-memory.md
- Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
- tutorials/get-started/5-customize-state.md
- tutorials/get-started/6-time-travel.md
- Local server: tutorials/langgraph-platform/local-server.md
- Deployment: cloud/quick_start.md
- General concepts:
- Common patterns:
- Agent architectures: concepts/agentic_concepts.md
- Workflows & agents: tutorials/workflows.md
- Agent development: agents/overview.md
- Workflow orchestration:
- Graph API: concepts/low_level.md
- Subgraphs: concepts/subgraphs.md
- Runtime: concepts/pregel.md
- Functional API: concepts/functional_api.md
- 1. Build a basic chatbot: tutorials/get-started/1-build-basic-chatbot.md
- 2. Add tools: tutorials/get-started/2-add-tools.md
- 3. Add memory: tutorials/get-started/3-add-memory.md
- 4. Add human-in-the-loop: tutorials/get-started/4-human-in-the-loop.md
- 5. Customize state: tutorials/get-started/5-customize-state.md
- 6. Time travel: tutorials/get-started/6-time-travel.md
- Run a local server: tutorials/langgraph-platform/local-server.md
- Agent development:
- Workflows & agents: tutorials/workflows.md
- Prebuilt components: agents/overview.md
- Run an agent: agents/run_agents.md
- Agent architectures: concepts/agentic_concepts.md
- Guides:
- LangGraph APIs:
- Graph API:
- Overview: concepts/low_level.md
- Use the Graph API: how-tos/graph-api.ipynb
- Functional API:
- Overview: concepts/functional_api.md
- Use the Functional API: how-tos/use-functional-api.md
- Runtime: concepts/pregel.md
- Core capabilities:
- Streaming: concepts/streaming.md
- Persistence: concepts/persistence.md
- Durable execution: concepts/durable_execution.md
- Memory: concepts/memory.md
- Tools: concepts/tools.md
- Human-in-the-loop: concepts/human_in_the_loop.md
- Breakpoints: concepts/breakpoints.md
- Time travel: concepts/time-travel.md
- Multi-agent: concepts/multi_agent.md
- Platform capabilities:
- Streaming:
- Overview: concepts/streaming.md
- Stream outputs: how-tos/streaming.md
- Use Server API: cloud/how-tos/streaming.md
- Persistence:
- Overview: concepts/persistence.md
- Durable execution:
- Overview: concepts/durable_execution.md
- Memory:
- Overview: concepts/memory.md
- Add memory: how-tos/memory/add-memory.md
- Context:
- Add context: agents/context.md
- Models:
- Configure model: agents/models.md
- Tools:
- Overview: concepts/tools.md
- Call tools: how-tos/tool-calling.md
- Human-in-the-loop:
- Overview: concepts/human_in_the_loop.md
- Add human intervention: how-tos/human_in_the_loop/add-human-in-the-loop.md
- Use Server API: cloud/how-tos/add-human-in-the-loop.md
- Breakpoints:
- Overview: concepts/breakpoints.md
- Set breakpoints: how-tos/human_in_the_loop/breakpoints.md
- Use Server API: cloud/how-tos/human_in_the_loop_breakpoint.md
- Time travel:
- Overview: concepts/time-travel.md
- Use time travel: how-tos/human_in_the_loop/time-travel.md
- Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md
- Subgraphs:
- Overview: concepts/subgraphs.md
- Use subgraphs: how-tos/subgraph.ipynb
- Multi-agent:
- Overview: concepts/multi_agent.md
- Prebuilt implementation: agents/multi-agent.md
- Custom implementation: how-tos/multi_agent.ipynb
- MCP:
- Use MCP: agents/mcp.md
- Server API: concepts/server-mcp.md
- Evaluation:
- Basic implementation: agents/evals.md
- Platform-only capabilities:
- LangGraph Platform:
- Overview: concepts/langgraph_platform.md
- Components:
@@ -133,97 +170,72 @@ nav:
- Data plane: concepts/langgraph_data_plane.md
- Control plane: concepts/langgraph_control_plane.md
- LangGraph CLI: concepts/langgraph_cli.md
- LangGraph Studio: concepts/langgraph_studio.md
- LangGraph Studio:
- Overview: concepts/langgraph_studio.md
- Quickstart: cloud/how-tos/studio/quick_start.md
- cloud/how-tos/invoke_studio.md
- cloud/how-tos/studio/manage_assistants.md
- cloud/how-tos/threads_studio.md
- cloud/how-tos/iterate_graph_studio.md
- cloud/how-tos/studio/run_evals.md
- cloud/how-tos/clone_traces_studio.md
- cloud/how-tos/datasets_studio.md
- LangGraph SDK: concepts/sdk.md
- Plans & pricing: concepts/plans.md
- Application structure: concepts/application_structure.md
- Scalability & resilience: concepts/scalability_and_resilience.md
- Authentication & access control: concepts/auth.md
- Assistants: concepts/assistants.md
- Double-texting: concepts/double_texting.md
- Webhooks: cloud/concepts/webhooks.md
- Cron jobs: cloud/concepts/cron_jobs.md
- Deployment:
- Overview: concepts/deployment_options.md
- Deployment options:
- Cloud SaaS: concepts/langgraph_cloud.md
- Self-Hosted Data Plane: concepts/langgraph_self_hosted_data_plane.md
- Self-Hosted Control Plane: concepts/langgraph_self_hosted_control_plane.md
- Standalone Container: concepts/langgraph_standalone_container.md
- Guides:
- Core Capabilities:
- Use the Graph API: how-tos/graph-api.ipynb
- Use the Functional API: how-tos/use-functional-api.md
- Models: agents/models.md
- Streaming: how-tos/streaming.md
- Context: agents/context.md
- Memory: how-tos/memory/add-memory.md
- Human-in-the-loop: how-tos/human_in_the_loop/add-human-in-the-loop.md
- Time travel: how-tos/human_in_the_loop/time-travel.md
- Breakpoints: how-tos/human_in_the_loop/breakpoints.md
- Tools: how-tos/tool-calling.md
- Subgraphs: how-tos/subgraph.ipynb
- Multi-agent:
- Prebuilt implementation: agents/multi-agent.md
- Custom implementation: how-tos/multi_agent.ipynb
- MCP: agents/mcp.md
- Evaluation:
- Basic implementation: agents/evals.md
- Platform capabilities:
- Deployment:
- Basic deployment: agents/deployment.md
- Set up your application:
- Use requirements.txt: cloud/deployment/setup.md
- Use pyproject.toml: cloud/deployment/setup_pyproject.md
- Use JavaScript: cloud/deployment/setup_javascript.md
- Use custom Docker: cloud/deployment/custom_docker.md
- Rebuild graph at runtime: cloud/deployment/graph_rebuild.md
- Deploy to production:
- Cloud SaaS: cloud/deployment/cloud.md
- Self-Hosted Data Plane: cloud/deployment/self_hosted_data_plane.md
- Self-Hosted Control Plane: cloud/deployment/self_hosted_control_plane.md
- Standalone Container: cloud/deployment/standalone_container.md
- LangGraph Studio:
- Quickstart: cloud/how-tos/studio/quick_start.md
- cloud/how-tos/invoke_studio.md
- cloud/how-tos/studio/manage_assistants.md
- cloud/how-tos/threads_studio.md
- cloud/how-tos/iterate_graph_studio.md
- cloud/how-tos/studio/run_evals.md
- cloud/how-tos/clone_traces_studio.md
- cloud/how-tos/datasets_studio.md
- Authentication & access control:
- Overview: concepts/auth.md
- how-tos/auth/custom_auth.md
- how-tos/auth/openapi_security.md
- Assistants:
- Overview: concepts/assistants.md
- cloud/how-tos/configuration_cloud.md
- Threads: cloud/how-tos/use_threads.md
- Runs:
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- cloud/how-tos/configurable_headers.md
- Double-texting:
- Overview: concepts/double_texting.md
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Webhooks:
- Overview: cloud/concepts/webhooks.md
- Use webhooks: cloud/how-tos/webhooks.md
- Cron jobs:
- Overview: cloud/concepts/cron_jobs.md
- cloud/how-tos/cron_jobs.md
- Server customization:
- how-tos/http/custom_lifespan.md
- how-tos/http/custom_middleware.md
- how-tos/http/custom_routes.md
- Authentication & access control:
- how-tos/auth/custom_auth.md
- how-tos/auth/openapi_security.md
- Data management:
- Add semantic search: cloud/deployment/semantic_search.md
- Add TTLs: how-tos/ttl/configure_ttl.md
- Assistants:
- cloud/how-tos/configuration_cloud.md
- Threads: cloud/how-tos/use_threads.md
- Runs:
- cloud/how-tos/background_run.md
- cloud/how-tos/same-thread.md
- cloud/how-tos/cron_jobs.md
- cloud/how-tos/stateless_runs.md
- cloud/how-tos/configurable_headers.md
- Double-texting:
- cloud/how-tos/interrupt_concurrent.md
- cloud/how-tos/rollback_concurrent.md
- cloud/how-tos/reject_concurrent.md
- cloud/how-tos/enqueue_concurrent.md
- Streaming: cloud/how-tos/streaming.md
- Human in the loop: cloud/how-tos/add-human-in-the-loop.md
- Time-travel: cloud/how-tos/human_in_the_loop_time_travel.md
- Breakpoints: cloud/how-tos/human_in_the_loop_breakpoint.md
- Webhooks: cloud/how-tos/webhooks.md
- Cron jobs: cloud/how-tos/cron_jobs.md
- MCP: concepts/server-mcp.md
- Add semantic search: cloud/deployment/semantic_search.md
- Add TTLs: how-tos/ttl/configure_ttl.md
- Deployment:
- Overview: concepts/deployment_options.md
- Quickstart: cloud/quick_start.md
- Set up your application:
- Use requirements.txt: cloud/deployment/setup.md
- Use pyproject.toml: cloud/deployment/setup_pyproject.md
- Use JavaScript: cloud/deployment/setup_javascript.md
- Use custom Docker: cloud/deployment/custom_docker.md
- Rebuild graph at runtime: cloud/deployment/graph_rebuild.md
- Deployment options:
- Cloud SaaS: concepts/langgraph_cloud.md
- Self-Hosted Data Plane: concepts/langgraph_self_hosted_data_plane.md
- Self-Hosted Control Plane: concepts/langgraph_self_hosted_control_plane.md
- Standalone Container: concepts/langgraph_standalone_container.md
- Deploy to production:
- Cloud SaaS: cloud/deployment/cloud.md
- Self-Hosted Data Plane: cloud/deployment/self_hosted_data_plane.md
- Self-Hosted Control Plane: cloud/deployment/self_hosted_control_plane.md
- Standalone Container: cloud/deployment/standalone_container.md
- Reference:
- reference/index.md
@@ -253,7 +265,6 @@ nav:
- Environment variables: cloud/reference/env_var.md
- Examples:
- agents/run_agents.md
- Template applications: concepts/template_applications.md # TODO: make tutorial
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.ipynb
- Agent Supervisor: tutorials/multi_agent/agent_supervisor.ipynb
@@ -278,6 +289,7 @@ nav:
- Case studies: adopters.md
- concepts/faq.md
- llms.txt: llms-txt-overview.md
- LangChain Forum: https://forum.langchain.com/
- Troubleshooting:
- Errors:
- troubleshooting/errors/index.md
Generated
+3 -3
View File
@@ -2590,7 +2590,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "0.5.0rc1"
version = "0.5.0"
source = { editable = "../libs/langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -2894,7 +2894,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "0.5.0rc0"
version = "0.5.1"
source = { editable = "../libs/prebuilt" }
dependencies = [
{ name = "langchain-core" },
@@ -2925,7 +2925,7 @@ dev = [
[[package]]
name = "langgraph-sdk"
version = "0.1.70"
version = "0.1.72"
source = { editable = "../libs/sdk-py" }
dependencies = [
{ name = "httpx" },
+1 -1
View File
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21",
"langgraph-checkpoint>=2.0.21,<3.0.0",
"orjson>=3.10.1",
"psycopg>=3.2.0",
"psycopg-pool>=3.2.0",
@@ -223,10 +223,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
Yields:
An SQLite cursor object.
"""
if not self.is_setup:
await self.setup()
async with self.lock:
if not self.is_setup:
await self.setup()
if transaction:
await self.conn.execute("BEGIN")
@@ -981,10 +981,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
Args:
transaction (bool): whether to use transaction for the DB operations
"""
if not self.is_setup:
self.setup()
with self.lock:
if not self.is_setup:
self.setup()
if transaction:
self.conn.execute("BEGIN")
@@ -1002,10 +1001,10 @@ class SqliteStore(BaseSqliteStore, BaseStore):
This method creates the necessary tables in the SQLite database if they don't
already exist and runs database migrations. It should be called before first use.
"""
if self.is_setup:
return
with self.lock:
if self.is_setup:
return
# Create migrations table if it doesn't exist
self.conn.executescript(
"""
+1 -1
View File
@@ -12,7 +12,7 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21",
"langgraph-checkpoint>=2.0.21,<3.0.0",
"aiosqlite>=0.20",
"sqlite-vec>=0.1.6",
]
+1 -1
View File
@@ -208,7 +208,7 @@ def up(
):
click.secho("Starting LangGraph API server...", fg="green")
click.secho(
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Platform closed beta.
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Platform.
For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.""",
)
with Runner() as runner, Progress(message="Pulling...") as set:
+2 -2
View File
@@ -18,8 +18,8 @@ dependencies = [
[project.optional-dependencies]
inmem = [
"langgraph-api>=0.2.67 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.3.0 ; python_version >= '3.11'",
"langgraph-api>=0.2.67,<0.3.0 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.3.0,<0.4.0 ; python_version >= '3.11'",
"python-dotenv>=0.8.0",
]
+2 -2
View File
@@ -530,8 +530,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.3.0" },
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67,<0.3.0" },
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.3.0,<0.4.0" },
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
{ name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" },
]
+1 -1
View File
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
+3 -3
View File
@@ -13,9 +13,9 @@ license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langchain-core>=0.1",
"langgraph-checkpoint>=2.1.0",
"langgraph-sdk>=0.1.42",
"langgraph-prebuilt>=0.5.0",
"langgraph-checkpoint>=2.1.0,<3.0.0",
"langgraph-sdk>=0.1.42,<0.2.0",
"langgraph-prebuilt>=0.5.0,<0.6.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
]
File diff suppressed because one or more lines are too long
+8 -8
View File
@@ -1183,7 +1183,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "0.3.63"
version = "0.3.67"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -1194,9 +1194,9 @@ dependencies = [
{ name = "tenacity" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/0a/b71a9a5d42e743d6876cce23d803e284b191ed4d6544e2f7fe1b37f7854c/langchain_core-0.3.63.tar.gz", hash = "sha256:e2e30cfbb7684a5a0319f6cbf065fc3c438bfd1060302f085a122527890fb01e", size = 558302, upload-time = "2025-05-29T18:57:19.933Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/71/a748861e6a69ab6ef50ab8e65120422a1f36245c71a0dd0f02de49c208e1/langchain_core-0.3.63-py3-none-any.whl", hash = "sha256:f91db8221b1bc6808f70b2e72fded1a94d50ee3f1dff1636fb5a5a514c64b7f5", size = 438468, upload-time = "2025-05-29T18:57:17.424Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
]
[[package]]
@@ -1423,7 +1423,7 @@ inmem = [
[[package]]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
source = { editable = "../prebuilt" }
dependencies = [
{ name = "langchain-core" },
@@ -1432,7 +1432,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.3.22" },
{ name = "langchain-core", specifier = ">=0.3.67" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
]
@@ -1497,7 +1497,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.3.43"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -1508,9 +1508,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/21/df84fe8b5c16971999650cbfc95a49f176d044a606e2b4eb957bbc122e1c/langsmith-0.3.43.tar.gz", hash = "sha256:7dab99b635859e24a1a252ad4f7e23170a45f4ea742567a10b4b26c50478ed43", size = 346328, upload-time = "2025-05-29T00:21:11.637Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/72/f5304de3e7e80e6dc266c161230aecb7895958f78b5af1541317d7615fc6/langsmith-0.3.43-py3-none-any.whl", hash = "sha256:2d4558068abf2eeb60ff80871187724e07f5e657d7d6be9e0c603df36c41140a", size = 361148, upload-time = "2025-05-29T00:21:08.759Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
@@ -30,7 +30,10 @@ from langchain_core.runnables.config import (
)
from langchain_core.tools import BaseTool, InjectedToolArg
from langchain_core.tools import tool as create_tool
from langchain_core.tools.base import get_all_basemodel_annotations
from langchain_core.tools.base import (
TOOL_MESSAGE_BLOCK_TYPES,
get_all_basemodel_annotations,
)
from pydantic import BaseModel
from typing_extensions import Annotated, get_args, get_origin
@@ -46,12 +49,11 @@ TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
def msg_content_output(output: Any) -> Union[str, list[dict]]:
recognized_content_block_types = ("image", "image_url", "text", "json")
if isinstance(output, str):
return output
elif isinstance(output, list) and all(
[
isinstance(x, dict) and x.get("type") in recognized_content_block_types
isinstance(x, dict) and x.get("type") in TOOL_MESSAGE_BLOCK_TYPES
for x in output
]
):
+3 -3
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
authors = []
requires-python = ">=3.9"
@@ -12,8 +12,8 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.1.0",
"langchain-core>=0.3.22",
"langgraph-checkpoint>=2.1.0,<3.0.0",
"langchain-core>=0.3.67",
]
[project.urls]
+8 -8
View File
@@ -302,7 +302,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "0.3.60"
version = "0.3.67"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -313,9 +313,9 @@ dependencies = [
{ name = "tenacity" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/75/95129aaada92980a002a31e002610a80af3c8967ae7884710372e89cdde0/langchain_core-0.3.60.tar.gz", hash = "sha256:63dd1bdf7939816115399522661ca85a2f3686a61440f2f46ebd86d1b028595b", size = 557456, upload-time = "2025-05-15T15:23:23.642Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/bc/344f5b11fdfe0e27f7064d2e829921a791461dc32e5ed285fe6325518c26/langchain_core-0.3.60-py3-none-any.whl", hash = "sha256:2ccdf06b12e699b1b0962bc02837056c075b4981c3d13f82a4d4c30bb22ea3dc", size = 437890, upload-time = "2025-05-15T15:23:22.278Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
]
[[package]]
@@ -464,7 +464,7 @@ dev = [
[[package]]
name = "langgraph-prebuilt"
version = "0.5.1"
version = "0.5.2"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -489,7 +489,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.3.22" },
{ name = "langchain-core", specifier = ">=0.3.67" },
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
]
@@ -537,7 +537,7 @@ dev = [
[[package]]
name = "langsmith"
version = "0.3.42"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -548,9 +548,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/44/fe171c0b0fb0377b191aebf0b7779e0c7b2a53693c6a01ddad737212495d/langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25", size = 345619, upload-time = "2025-05-03T03:07:17.873Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334, upload-time = "2025-05-03T03:07:15.491Z" },
{ url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" },
]
[[package]]
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.86",
"version": "0.0.88",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -22,7 +22,9 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@langchain/core": "^0.3.31",
"@langchain/langgraph-api": "~0.0.41",
"@langchain/core": "^0.3.61",
"@langchain/langgraph": "^0.3.5",
"@langchain/scripts": "^0.1.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -35,6 +37,7 @@
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react": "^4.4.1",
"concat-md": "^0.5.1",
"hono": "^4.8.2",
"jsdom": "^26.1.0",
"msw": "^2.8.2",
"prettier": "^3.2.5",
+34 -4
View File
@@ -1014,8 +1014,8 @@ export class RunsClient<
const stream: ReadableStream<{ event: any; data: any }> = (
response.body || new ReadableStream({ start: (ctrl) => ctrl.close() })
)
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
yield* IterableReadableStream.fromReadableStream(stream);
}
@@ -1318,8 +1318,8 @@ export class RunsClient<
const stream: ReadableStream<{ event: string; data: any }> = (
response.body || new ReadableStream({ start: (ctrl) => ctrl.close() })
)
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
yield* IterableReadableStream.fromReadableStream(stream);
}
@@ -1658,7 +1658,30 @@ export class Client<
*/
public "~ui": UiClient;
/**
* @internal Used to obtain a stable key representing the client.
*/
private "~configHash": string | undefined;
constructor(config?: ClientConfig) {
this["~configHash"] = (() =>
JSON.stringify({
apiUrl: config?.apiUrl,
apiKey: config?.apiKey,
timeoutMs: config?.timeoutMs,
defaultHeaders: config?.defaultHeaders,
maxConcurrency: config?.callerOptions?.maxConcurrency,
maxRetries: config?.callerOptions?.maxRetries,
callbacks: {
onFailedResponseHook:
config?.callerOptions?.onFailedResponseHook != null,
onRequest: config?.onRequest != null,
fetch: config?.callerOptions?.fetch != null,
},
}))();
this.assistants = new AssistantsClient(config);
this.threads = new ThreadsClient(config);
this.runs = new RunsClient(config);
@@ -1667,3 +1690,10 @@ export class Client<
this["~ui"] = new UiClient(config);
}
}
/**
* @internal Used to obtain a stable key representing the client.
*/
export function getClientConfigHash(client: Client): string | undefined {
return client["~configHash"];
}
+131 -28
View File
@@ -1,7 +1,7 @@
/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */
"use client";
import { Client, type ClientConfig } from "../client.js";
import { Client, getClientConfigHash, type ClientConfig } from "../client.js";
import type {
Command,
DisconnectMode,
@@ -31,7 +31,7 @@ import type {
} from "../types.stream.js";
import {
type MutableRefObject,
type RefObject,
useCallback,
useEffect,
useMemo,
@@ -316,16 +316,21 @@ function fetchHistory<StateType extends Record<string, unknown>>(
function useThreadHistory<StateType extends Record<string, unknown>>(
threadId: string | undefined | null,
client: Client,
clearCallbackRef: MutableRefObject<(() => void) | undefined>,
submittingRef: MutableRefObject<boolean>,
clearCallbackRef: RefObject<(() => void) | undefined>,
submittingRef: RefObject<boolean>,
) {
const [history, setHistory] = useState<ThreadState<StateType>[]>([]);
const clientHash = getClientConfigHash(client);
const clientRef = useRef(client);
clientRef.current = client;
const fetcher = useCallback(
(
threadId: string | undefined | null,
): Promise<ThreadState<StateType>[]> => {
if (threadId != null) {
const client = clientRef.current;
return fetchHistory<StateType>(client, threadId).then((history) => {
setHistory(history);
return history;
@@ -342,7 +347,7 @@ function useThreadHistory<StateType extends Record<string, unknown>>(
useEffect(() => {
if (submittingRef.current) return;
fetcher(threadId);
}, [fetcher, submittingRef, threadId]);
}, [fetcher, clientHash, submittingRef, threadId]);
return {
data: history,
@@ -506,6 +511,31 @@ export interface UseStreamOptions<
*/
onDebugEvent?: (data: DebugStreamEvent["data"]) => void;
/**
* Callback that is called when the stream is stopped by the user.
* Provides a mutate function to update the stream state immediately
* without requiring a server roundtrip.
*
* @example
* ```typescript
* onStop: ({ mutate }) => {
* mutate((prev) => ({
* ...prev,
* ui: prev.ui?.map(component =>
* component.props.isLoading
* ? { ...component, props: { ...component.props, stopped: true, isLoading: false }}
* : component
* )
* }));
* }
* ```
*/
onStop?: (options: {
mutate: (
update: Partial<StateType> | ((prev: StateType) => Partial<StateType>),
) => void;
}) => void;
/**
* The ID of the thread to fetch history and current values from.
*/
@@ -518,6 +548,17 @@ export interface UseStreamOptions<
/** Will reconnect the stream on mount */
reconnectOnMount?: boolean | (() => RunMetadataStorage);
/**
* Initial values to display immediately when loading a thread.
* Useful for displaying cached thread data while official history loads.
* These values will be replaced when official thread data is fetched.
*
* Note: UI components from initialValues will render immediately if they're
* predefined in LoadExternalComponent's components prop, providing instant
* cached UI display without server fetches.
*/
initialValues?: StateType | null;
}
interface RunMetadataStorage {
@@ -616,7 +657,11 @@ export interface UseStream<
/**
* Join an active stream.
*/
joinStream: (runId: string) => Promise<void>;
joinStream: (
runId: string,
lastEventId?: string,
options?: { streamMode?: StreamMode | StreamMode[] },
) => Promise<void>;
}
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
@@ -647,6 +692,61 @@ interface SubmitOptions<
*/
streamSubgraphs?: boolean;
streamResumable?: boolean;
/**
* The ID to use when creating a new thread. When provided, this ID will be used
* for thread creation when threadId is `null` or `undefined`.
* This enables optimistic UI updates where you know the thread ID
* before the thread is actually created.
*/
threadId?: string;
}
function useStreamValuesState<StateType extends Record<string, unknown>>() {
type Kind = "stream" | "stop";
type Values = StateType | null;
type Update = Values | ((prev: Values, kind?: Kind) => Values);
type Mutate = Partial<StateType> | ((prev: StateType) => Partial<StateType>);
const [values, setValues] = useState<[values: StateType, kind: Kind] | null>(
null,
);
const setStreamValues = useCallback(
(values: Update, kind: Kind = "stream") => {
if (typeof values === "function") {
setValues((prevTuple) => {
const [prevValues, prevKind] = prevTuple ?? [null, "stream"];
const next = values(prevValues, prevKind);
if (next == null) return null;
return [next, kind] as [StateType, Kind];
});
return;
}
if (values == null) setValues(null);
setValues([values, kind] as [StateType, Kind]);
},
[],
);
const mutate = useCallback(
(kind: Kind, serverValues: StateType) => (update: Mutate) => {
setStreamValues((clientValues) => {
const prev = { ...serverValues, ...clientValues };
const next = typeof update === "function" ? update(prev) : update;
return { ...prev, ...next };
}, kind);
},
[setStreamValues],
);
return [values?.[0] ?? null, setStreamValues, mutate] as [
Values,
(update: Update, kind?: Kind) => void,
(kind: Kind, serverValues: StateType) => (update: Mutate) => void,
];
}
export function useStream<
@@ -712,7 +812,8 @@ export function useStream<
const [isLoading, setIsLoading] = useState(false);
const [streamError, setStreamError] = useState<unknown>(undefined);
const [streamValues, setStreamValues] = useState<StateType | null>(null);
const [streamValues, setStreamValues, getMutateFn] =
useStreamValuesState<StateType>();
const messageManagerRef = useRef(new MessageTupleManager());
const submittingRef = useRef(false);
@@ -783,7 +884,9 @@ export function useStream<
);
const threadHead: ThreadState<StateType> | undefined = flatHistory.at(-1);
const historyValues = threadHead?.values ?? ({} as StateType);
const historyValues =
threadHead?.values ?? options.initialValues ?? ({} as StateType);
const historyError = (() => {
const error = threadHead?.tasks?.at(-1)?.error;
if (error == null) return undefined;
@@ -848,6 +951,8 @@ export function useStream<
if (runId) client.runs.cancel(threadId, runId);
runMetadataStorage.removeItem(`lg:stream:${threadId}`);
}
options?.onStop?.({ mutate: getMutateFn("stop", historyValues) });
};
async function consumeStream(
@@ -880,15 +985,7 @@ export function useStream<
if (event === "updates") options.onUpdateEvent?.(data);
if (event === "custom")
options.onCustomEvent?.(data, {
mutate: (update) =>
setStreamValues((prev) => {
// should not happen
if (prev == null) return prev;
return {
...prev,
...(typeof update === "function" ? update(prev) : update),
};
}),
mutate: getMutateFn("stream", historyValues),
});
if (event === "metadata") options.onMetadataEvent?.(data);
if (event === "events") options.onLangChainEvent?.(data);
@@ -930,8 +1027,11 @@ export function useStream<
// TODO: stream created checkpoints to avoid an unnecessary network request
const result = await run.onSuccess();
setStreamValues(null);
setStreamValues((values, kind) => {
// Do not clear out the user values set on `stop`.
if (kind === "stop") return values;
return null;
});
if (streamError != null) throw streamError;
const lastHead = result.at(0);
@@ -957,13 +1057,18 @@ export function useStream<
}
}
const joinStream = async (runId: string, lastEventId?: string) => {
const joinStream = async (
runId: string,
lastEventId?: string,
options?: { streamMode?: StreamMode | StreamMode[] },
) => {
lastEventId ??= "-1";
if (!threadId) return;
await consumeStream(async (signal: AbortSignal) => {
const stream = client.runs.joinStream(threadId, runId, {
signal,
lastEventId,
streamMode: options?.streamMode,
}) as AsyncGenerator<EventStreamEvent>;
return {
@@ -989,26 +1094,24 @@ export function useStream<
if (newPath != null) setBranch(newPath ?? "");
// Assumption: we're setting the initial value
// Used for instant feedback
setStreamValues(() => {
const values = { ...historyValues };
if (submitOptions?.optimisticValues != null) {
return {
...values,
...historyValues,
...(typeof submitOptions.optimisticValues === "function"
? submitOptions.optimisticValues(values)
? submitOptions.optimisticValues(historyValues)
: submitOptions.optimisticValues),
};
}
return values;
return { ...historyValues };
});
let usableThreadId = threadId;
if (!usableThreadId) {
const thread = await client.threads.create();
const thread = await client.threads.create({
threadId: submitOptions?.threadId,
});
onThreadId(thread.thread_id);
usableThreadId = thread.thread_id;
}
+16 -16
View File
@@ -20,7 +20,7 @@ describe("BytesLineDecoder", () => {
test("handles single line with newline", async () => {
const input = createStream([textEncoder.encode("hello\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -29,7 +29,7 @@ describe("BytesLineDecoder", () => {
test("handles multiple lines", async () => {
const input = createStream([textEncoder.encode("line1\nline2\nline3\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(3);
@@ -44,7 +44,7 @@ describe("BytesLineDecoder", () => {
textEncoder.encode("ne1\nli"),
textEncoder.encode("ne2\n"),
]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -54,7 +54,7 @@ describe("BytesLineDecoder", () => {
test("handles CR LF line endings", async () => {
const input = createStream([textEncoder.encode("line1\r\nline2\r\n")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -67,7 +67,7 @@ describe("BytesLineDecoder", () => {
textEncoder.encode("line1\r"),
textEncoder.encode("\nline2\r\n"),
]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -77,7 +77,7 @@ describe("BytesLineDecoder", () => {
test("handles stale line", async () => {
const input = createStream([textEncoder.encode("hello")]);
const decoded = input.pipeThrough(new BytesLineDecoder());
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -99,8 +99,8 @@ describe("SSEDecoder", () => {
"\n",
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -117,8 +117,8 @@ describe("SSEDecoder", () => {
'data: {"message": "hello"}\n',
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -138,8 +138,8 @@ describe("SSEDecoder", () => {
"\n",
]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
@@ -156,8 +156,8 @@ describe("SSEDecoder", () => {
test("end event without data", async () => {
const input = createStream(["event: test\n"]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
@@ -170,8 +170,8 @@ describe("SSEDecoder", () => {
test("end event without newline", async () => {
const input = createStream(["event: end"]);
const decoded = input
.pipeThrough(new BytesLineDecoder())
.pipeThrough(new SSEDecoder());
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
+361 -354
View File
@@ -1,14 +1,58 @@
import "@testing-library/jest-dom/vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import { http } from "msw";
import { useStream } from "../react/stream.js";
import "@testing-library/jest-dom/vitest";
import type { Message } from "../types.messages.js";
import { StateGraph, MessagesAnnotation, START } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph-checkpoint";
import { FakeStreamingChatModel } from "@langchain/core/utils/testing";
import { AIMessage } from "@langchain/core/messages";
import { createEmbedServer } from "@langchain/langgraph-api/experimental/embed";
import { randomUUID } from "node:crypto";
import { useState } from "react";
const threads = (() => {
const THREADS: Record<
string,
{ thread_id: string; metadata: Record<string, unknown> }
> = {};
return {
get: async (id: string) => THREADS[id],
put: async (
threadId: string,
{ metadata }: { metadata?: Record<string, unknown> },
) => {
THREADS[threadId] = { thread_id: threadId, metadata: metadata ?? {} };
},
delete: async (threadId: string) => {
delete THREADS[threadId];
},
};
})();
const checkpointer = new MemorySaver();
const model = new FakeStreamingChatModel({ responses: [new AIMessage("Hey")] });
const agent = new StateGraph(MessagesAnnotation)
.addNode("agent", async (state: { messages: Message[] }) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
})
.addEdge(START, "agent")
.compile();
const app = createEmbedServer({ graph: { agent }, checkpointer, threads });
const server = setupServer(http.all("*", (ctx) => app.fetch(ctx.request)));
function TestChatComponent() {
const { messages, isLoading, error, submit, stop } = useStream({
assistantId: "test-assistant",
assistantId: "agent",
apiKey: "test-api-key",
});
@@ -42,353 +86,6 @@ function TestChatComponent() {
);
}
// Mock server setup
const server = setupServer(
// Mock thread creation
http.post("*/threads", () => {
return HttpResponse.json({ thread_id: "test-thread-id" });
}),
// Mock stream endpoint
http.post("*/threads/:threadId/runs/stream", async () => {
const encoder = new TextEncoder();
const sendSSE = (event: string, data: unknown) =>
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
const stream = new ReadableStream({
async start(controller) {
await new Promise((resolve) => setTimeout(resolve, 10));
controller.enqueue(
sendSSE("metadata", {
run_id: "1f03278a-1734-6518-80a4-3390db59f960",
attempt: 1,
}),
);
controller.enqueue(
sendSSE("values", {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
],
}),
);
controller.enqueue(
sendSSE("messages", [
{
content: "",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "Hello",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "! How can I assist you today?",
additional_kwargs: {},
response_metadata: { model_name: "claude-3-7-sonnet-latest" },
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("messages", [
{
content: "",
additional_kwargs: {},
response_metadata: {
stop_reason: "end_turn",
stop_sequence: null,
},
type: "AIMessageChunk",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
tool_call_chunks: [],
},
{ run_attempt: 1 },
]),
);
controller.enqueue(
sendSSE("values", {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
tool_calls: [],
invalid_tool_calls: [],
},
],
}),
);
controller.close();
},
});
server.use(
http.post("*/threads/:threadId/history", () => {
return HttpResponse.json([
{
values: {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
next: [],
tasks: [],
metadata: {
run_attempt: 1,
source: "loop",
writes: {
agent: {
messages: [
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
},
step: 1,
parents: {},
},
created_at: "2025-05-16T17:10:16.987537+00:00",
checkpoint: {
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: {
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
checkpoint_id: "1f03278a-38cf-6c68-8001-22b77ac43ff6",
parent_checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
},
{
values: {
messages: [
{
content: "Hey",
additional_kwargs: {},
response_metadata: {},
type: "human",
name: null,
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
example: false,
},
],
},
next: ["agent"],
tasks: [
{
id: "e1b7b52b-a78e-4b32-0c89-e06bf46405ed",
name: "agent",
path: ["__pregel_pull", "agent"],
error: null,
interrupts: [],
checkpoint: null,
state: null,
result: {
messages: [
{
content: "Hello! How can I assist you today?",
additional_kwargs: {},
response_metadata: {
model_name: "claude-3-7-sonnet-latest",
stop_reason: "end_turn",
stop_sequence: null,
},
type: "ai",
name: null,
id: "run-3e90ba6a-71d6-49e7-94a8-6bcac2fd0f40",
example: false,
tool_calls: [],
invalid_tool_calls: [],
},
],
},
},
],
metadata: {
run_attempt: 1,
},
created_at: "2025-05-16T17:10:14.429889+00:00",
checkpoint: {
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: {
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
checkpoint_id: "1f03278a-206b-67c6-8000-ac34a0872e1a",
parent_checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
},
{
values: {
messages: [],
},
next: ["__start__"],
tasks: [
{
id: "291af033-2ddc-3320-8bbc-28060057cae5",
name: "__start__",
path: ["__pregel_pull", "__start__"],
error: null,
interrupts: [],
checkpoint: null,
state: null,
result: {
messages: [
{
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
type: "human",
content: "Hey",
},
],
},
},
],
metadata: {
run_attempt: 1,
source: "input",
writes: {
__start__: {
messages: [
{
id: "2d8c0d9f-a614-4e44-b474-6a56e9471cf5",
type: "human",
content: "Hey",
},
],
},
},
step: -1,
parents: {},
},
created_at: "2025-05-16T17:10:14.428191+00:00",
checkpoint: {
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
thread_id: "b06fd92a-955c-446e-b233-7977716c4a9c",
checkpoint_ns: "",
},
parent_checkpoint: null,
checkpoint_id: "1f03278a-2067-6590-bfff-3fb740466fc3",
parent_checkpoint_id: null,
},
]);
}),
);
return new HttpResponse(stream, {
headers: { "Content-Type": "text/event-stream" },
});
}),
);
server.use;
describe("useStream", () => {
beforeEach(() => server.listen());
@@ -417,10 +114,8 @@ describe("useStream", () => {
// Wait for messages to appear
await waitFor(() => {
expect(screen.getByTestId("message-0")).toHaveTextContent("Hey");
expect(screen.getByTestId("message-1")).toHaveTextContent(
"Hello! How can I assist you today?",
);
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
});
// Check final state
@@ -440,4 +135,316 @@ describe("useStream", () => {
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
});
});
it("displays initial values immediately and clears them when submitting", async () => {
const user = userEvent.setup();
function TestCachedComponent() {
const { messages, values, submit } = useStream<{
messages: Message[];
}>({
assistantId: "agent",
apiKey: "test-api-key",
initialValues: {
messages: [
{ id: "cached-1", type: "human", content: "Cached user message" },
{ id: "cached-2", type: "ai", content: "Cached AI response" },
],
},
});
return (
<div>
<div data-testid="messages">
{messages.map((msg, i) => (
<div
key={msg.id ?? i}
data-testid={
msg.id?.includes("cached")
? `message-cached-${i}`
: `message-${i}`
}
>
{typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)}
</div>
))}
</div>
<div data-testid="values">{JSON.stringify(values)}</div>
<button
data-testid="submit"
onClick={() =>
submit({ messages: [{ content: "Hello", type: "human" }] })
}
>
Submit
</button>
</div>
);
}
render(<TestCachedComponent />);
// Should immediately show cached messages
expect(screen.getByTestId("message-cached-0")).toHaveTextContent(
"Cached user message",
);
expect(screen.getByTestId("message-cached-1")).toHaveTextContent(
"Cached AI response",
);
// Values should include initial values
expect(screen.getByTestId("values")).toHaveTextContent(
"Cached user message",
);
// Submitting should clear out the cached messages
await user.click(screen.getByTestId("submit"));
// Wait for messages to appear
await waitFor(() => {
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
});
});
it("accepts newThreadId option without errors", async () => {
const user = userEvent.setup();
const spy = vi.fn();
const predeterminedThreadId = randomUUID();
// Test that newThreadId option can be passed without causing errors
function TestNewThreadComponent() {
const stream = useStream<{ messages: Message[] }>({
assistantId: "agent",
apiKey: "test-api-key",
threadId: null, // Start with no thread
onThreadId: spy, // Mock callback
});
return (
<div>
<div data-testid="loading">
{stream.isLoading ? "Loading..." : "Not loading"}
</div>
<div data-testid="thread-id">
{stream.client ? "Client ready" : "No client"}
</div>
<button
data-testid="submit"
onClick={() =>
stream.submit({}, { threadId: predeterminedThreadId })
}
>
Submit
</button>
</div>
);
}
render(<TestNewThreadComponent />);
// Should render without errors
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
expect(screen.getByTestId("thread-id")).toHaveTextContent("Client ready");
await user.click(screen.getByTestId("submit"));
expect(spy).toHaveBeenCalledWith(predeterminedThreadId);
expect(await threads.get(predeterminedThreadId)).toEqual({
thread_id: predeterminedThreadId,
metadata: {
graph_id: "agent",
assistant_id: "agent",
},
});
});
it("onStop callback is called when stop is called", async () => {
const user = userEvent.setup();
const onStopCallback = vi.fn();
function TestComponent() {
const { submit, stop } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
onStop: onStopCallback,
});
return (
<div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Start a stream and stop it
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify onStop was called with mutate function
expect(onStopCallback).toHaveBeenCalledTimes(1);
expect(onStopCallback).toHaveBeenCalledWith(
expect.objectContaining({
mutate: expect.any(Function),
}),
);
});
it("onStop mutate function updates stream values immediately", async () => {
const user = userEvent.setup();
function TestComponent() {
const [stopped, setStopped] = useState(false);
const { submit, stop, messages } = useStream<{ messages: Message[] }>({
assistantId: "agent",
apiKey: "test-api-key",
onStop: ({ mutate }) => {
setStopped(true);
mutate((prev) => ({
...prev,
messages: [
...(prev.messages ?? []),
{ type: "ai", content: "Stream stopped" },
],
}));
},
});
return (
<div>
<div data-testid="stopped-status">
{stopped ? "Stopped" : "Not stopped"}
</div>
<div data-testid="messages">
{messages.map((msg, i) => (
<div key={msg.id ?? i} data-testid={`message-${i}`}>
{typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content)}
</div>
))}
</div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Initial state
expect(screen.getByTestId("stopped-status")).toHaveTextContent(
"Not stopped",
);
// Start and stop stream
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify state was updated immediately
await waitFor(() => {
expect(screen.getByTestId("stopped-status")).toHaveTextContent("Stopped");
expect(screen.getByTestId("message-0")).toHaveTextContent(
"Stream stopped",
);
});
});
it("onStop handles functional updates correctly", async () => {
const user = userEvent.setup();
function TestComponent() {
const { submit, stop, values } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
initialValues: {
counter: 5,
items: ["item1", "item2"],
},
onStop: ({ mutate }) => {
mutate((prev: any) => ({
...prev,
counter: (prev.counter || 0) + 10,
items: [...(prev.items || []), "stopped"],
}));
},
});
return (
<div>
<div data-testid="counter">{(values as any).counter}</div>
<div data-testid="items">{(values as any).items?.join(", ")}</div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
render(<TestComponent />);
// Initial state
expect(screen.getByTestId("counter")).toHaveTextContent("5");
expect(screen.getByTestId("items")).toHaveTextContent("item1, item2");
// Start and stop stream
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Verify functional update was applied correctly
await waitFor(() => {
expect(screen.getByTestId("counter")).toHaveTextContent("15");
expect(screen.getByTestId("items")).toHaveTextContent(
"item1, item2, stopped",
);
});
});
it("onStop is not called when stream completes naturally", async () => {
const user = userEvent.setup();
const onStopCallback = vi.fn();
function TestComponent() {
const { submit } = useStream({
assistantId: "agent",
apiKey: "test-api-key",
onStop: onStopCallback,
});
return (
<div>
<button data-testid="submit" onClick={() => submit({})}>
Send
</button>
</div>
);
}
render(<TestComponent />);
// Start a stream and let it complete naturally
await user.click(screen.getByTestId("submit"));
// Wait for stream to complete naturally
await waitFor(() => {
expect(onStopCallback).not.toHaveBeenCalled();
});
});
});
+16 -23
View File
@@ -13,16 +13,22 @@ type MessageContent = string | MessageContentComplex[];
*/
type MessageAdditionalKwargs = Record<string, unknown>;
export type HumanMessage = {
type: "human";
id?: string | undefined;
type BaseMessage = {
additional_kwargs?: MessageAdditionalKwargs | undefined;
content: MessageContent;
id?: string | undefined;
name?: string | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type AIMessage = {
export type HumanMessage = BaseMessage & {
type: "human";
example?: boolean | undefined;
};
export type AIMessage = BaseMessage & {
type: "ai";
id?: string | undefined;
content: MessageContent;
example?: boolean | undefined;
tool_calls?:
| {
name: string;
@@ -57,19 +63,12 @@ export type AIMessage = {
| undefined;
}
| undefined;
additional_kwargs?: MessageAdditionalKwargs | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type ToolMessage = {
export type ToolMessage = BaseMessage & {
type: "tool";
name?: string | undefined;
id?: string | undefined;
content: MessageContent;
status?: "error" | "success" | undefined;
tool_call_id: string;
additional_kwargs?: MessageAdditionalKwargs | undefined;
response_metadata?: Record<string, unknown> | undefined;
/**
* Artifact of the Tool execution which is not meant to be sent to the model.
*
@@ -81,22 +80,16 @@ export type ToolMessage = {
artifact?: any;
};
export type SystemMessage = {
export type SystemMessage = BaseMessage & {
type: "system";
id?: string | undefined;
content: MessageContent;
};
export type FunctionMessage = {
export type FunctionMessage = BaseMessage & {
type: "function";
id?: string | undefined;
content: MessageContent;
};
export type RemoveMessage = {
export type RemoveMessage = BaseMessage & {
type: "remove";
id: string;
content: MessageContent;
};
export type Message =
+119 -123
View File
@@ -6,90 +6,88 @@ const SPACE = " ".charCodeAt(0);
const TRAILING_NEWLINE = [CR, LF];
export class BytesLineDecoder extends TransformStream<Uint8Array, Uint8Array> {
constructor() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
export function BytesLineDecoder() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
super({
start() {
buffer = [];
return new TransformStream<Uint8Array, Uint8Array>({
start() {
buffer = [];
trailingCr = false;
},
transform(chunk, controller) {
// See https://docs.python.org/3/glossary.html#term-universal-newlines
let text = chunk;
// Handle trailing CR from previous chunk
if (trailingCr) {
text = joinArrays([[CR], text]);
trailingCr = false;
},
}
transform(chunk, controller) {
// See https://docs.python.org/3/glossary.html#term-universal-newlines
let text = chunk;
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
// Handle trailing CR from previous chunk
if (trailingCr) {
text = joinArrays([[CR], text]);
trailingCr = false;
}
if (!text.length) return;
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
const lastIdx = text.length - 1;
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
(acc, cur, idx) => {
if (acc.from > idx) return acc;
if (!text.length) return;
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
const lastIdx = text.length - 1;
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
(acc, cur, idx) => {
if (acc.from > idx) return acc;
if (cur === CR || cur === LF) {
acc.lines.push(text.subarray(acc.from, idx));
if (cur === CR && text[idx + 1] === LF) {
acc.from = idx + 2;
} else {
acc.from = idx + 1;
}
if (cur === CR || cur === LF) {
acc.lines.push(text.subarray(acc.from, idx));
if (cur === CR && text[idx + 1] === LF) {
acc.from = idx + 2;
} else {
acc.from = idx + 1;
}
}
if (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
if (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
return acc;
},
{ lines: [], from: 0 },
);
return acc;
},
{ lines: [], from: 0 },
);
if (lines.length === 1 && !trailingNewline) {
buffer.push(lines[0]);
return;
}
if (lines.length === 1 && !trailingNewline) {
buffer.push(lines[0]);
return;
}
if (buffer.length) {
// Include existing buffer in first line
buffer.push(lines[0]);
lines[0] = joinArrays(buffer);
buffer = [];
}
if (buffer.length) {
// Include existing buffer in first line
buffer.push(lines[0]);
lines[0] = joinArrays(buffer);
buffer = [];
}
if (!trailingNewline) {
// If the last segment is not newline terminated,
// buffer it for the next chunk
if (lines.length) buffer = [lines.pop()!];
}
if (!trailingNewline) {
// If the last segment is not newline terminated,
// buffer it for the next chunk
if (lines.length) buffer = [lines.pop()!];
}
// Enqueue complete lines
for (const line of lines) {
controller.enqueue(line);
}
},
// Enqueue complete lines
for (const line of lines) {
controller.enqueue(line);
}
},
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
interface StreamPart {
@@ -98,69 +96,67 @@ interface StreamPart {
data: unknown;
}
export class SSEDecoder extends TransformStream<Uint8Array, StreamPart> {
constructor() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
export function SSEDecoder() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
const decoder = new TextDecoder();
const decoder = new TextDecoder();
super({
transform(chunk, controller) {
// Handle empty line case
if (!chunk.length) {
if (!event && !data.length && !lastEventId && retry == null) return;
return new TransformStream<Uint8Array, StreamPart>({
transform(chunk, controller) {
// Handle empty line case
if (!chunk.length) {
if (!event && !data.length && !lastEventId && retry == null) return;
const sse = {
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
};
const sse = {
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
};
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
controller.enqueue(sse);
return;
}
controller.enqueue(sse);
return;
}
// Ignore comments
if (chunk[0] === COLON) return;
// Ignore comments
if (chunk[0] === COLON) return;
const sepIdx = chunk.indexOf(COLON);
if (sepIdx === -1) return;
const sepIdx = chunk.indexOf(COLON);
if (sepIdx === -1) return;
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
let value = chunk.subarray(sepIdx + 1);
if (value[0] === SPACE) value = value.subarray(1);
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
let value = chunk.subarray(sepIdx + 1);
if (value[0] === SPACE) value = value.subarray(1);
if (fieldName === "event") {
event = decoder.decode(value);
} else if (fieldName === "data") {
data.push(value);
} else if (fieldName === "id") {
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
} else if (fieldName === "retry") {
const retryNum = Number.parseInt(decoder.decode(value));
if (!Number.isNaN(retryNum)) retry = retryNum;
}
},
if (fieldName === "event") {
event = decoder.decode(value);
} else if (fieldName === "data") {
data.push(value);
} else if (fieldName === "id") {
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
} else if (fieldName === "retry") {
const retryNum = Number.parseInt(decoder.decode(value));
if (!Number.isNaN(retryNum)) retry = retryNum;
}
},
flush(controller) {
if (event) {
controller.enqueue({
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
});
}
},
});
}
flush(controller) {
if (event) {
controller.enqueue({
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
});
}
},
});
}
function joinArrays(data: ArrayLike<number>[]) {
+1046 -22
View File
File diff suppressed because it is too large Load Diff