chore: move sdk-js to langgraphjs (#5334)

This commit is contained in:
David Duong
2025-07-03 16:58:31 +02:00
committed by GitHub
45 changed files with 3408 additions and 12140 deletions
-55
View File
@@ -24,7 +24,6 @@ jobs:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v4
@@ -40,8 +39,6 @@ jobs:
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/prebuilt/**'
sdk-js:
- 'libs/sdk-js/**'
deps:
- '**/pyproject.toml'
- '**/uv.lock'
@@ -152,68 +149,16 @@ jobs:
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
lint-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js (LTS)
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Run lint
run: yarn lint
- name: Build
run: yarn build
test-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js (LTS)
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Run tests
run: yarn test
ci_success:
name: "CI Success"
needs:
[
lint,
lint-js,
test,
test-langgraph,
check-sdk-methods,
check-schema,
integration-test,
test-js,
]
if: |
always()
-41
View File
@@ -1,41 +0,0 @@
name: JS Release
on:
workflow_dispatch:
permissions:
contents: read
jobs:
publish:
# Disallow publishing from branches that aren't `main`.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v4
# JS Build
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Publish package to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
npm publish
-1
View File
@@ -1,4 +1,3 @@
site/
docs/cloud/reference/sdk/js_ts_sdk_ref.md
.vercel
+3 -9
View File
@@ -1,10 +1,4 @@
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
build-typedoc:
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
# Add links to the monorepo
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell llms-text build-prebuilt tests
build-prebuilt:
# Use to create an update to date prebuilt page.
@@ -21,7 +15,7 @@ build-prebuilt:
fi
uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python
build-docs: build-typedoc build-prebuilt
build-docs: build-prebuilt
uv run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
@@ -45,7 +39,7 @@ vercel-build-docs: install-vercel-deps
serve-clean-docs: clean-docs
uv run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
serve-docs: build-typedoc
serve-docs:
uv run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
clean-docs:
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -263,7 +263,7 @@ nav:
- Control Plane API: cloud/reference/api/api_ref_control_plane.md
- CLI: cloud/reference/cli.md
- SDK (Python): cloud/reference/sdk/python_sdk_ref.md
- SDK (JS/TS): cloud/reference/sdk/js_ts_sdk_ref.md
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
- RemoteGraph: reference/remote_graph.md
- Environment variables: cloud/reference/env_var.md
-28
View File
@@ -1,28 +0,0 @@
index.cjs
index.js
index.d.ts
index.d.cts
client.cjs
client.js
client.d.ts
client.d.cts
auth.cjs
auth.js
auth.d.ts
auth.d.cts
react.cjs
react.js
react.d.ts
react.d.cts
react-ui.cjs
react-ui.js
react-ui.d.ts
react-ui.d.cts
react-ui/server.cjs
react-ui/server.js
react-ui/server.d.ts
react-ui/server.d.cts
node_modules
dist
.yarn
docs
-1
View File
@@ -1 +0,0 @@
{}
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -64
View File
@@ -1,64 +1 @@
# LangGraph JS/TS SDK
This repository contains the JS/TS SDK for interacting with the LangGraph REST API.
## Quick Start
To get started with the JS/TS SDK, [install the package](https://www.npmjs.com/package/@langchain/langgraph-sdk)
```bash
yarn add @langchain/langgraph-sdk
```
You will need a running LangGraph API server. If you're running a server locally using `langgraph-cli`, SDK will automatically point at `http://localhost:8123`, otherwise
you would need to specify the server URL when creating a client.
```js
import { Client } from "@langchain/langgraph-sdk";
const client = new Client();
// List all assistants
const assistants = await client.assistants.search({
metadata: null,
offset: 0,
limit: 10,
});
// We auto-create an assistant for each graph you register in config.
const agent = assistants[0];
// Start a new thread
const thread = await client.threads.create();
// Start a streaming run
const messages = [{ role: "human", content: "what's the weather in la" }];
const streamResponse = client.runs.stream(
thread["thread_id"],
agent["assistant_id"],
{
input: { messages },
}
);
for await (const chunk of streamResponse) {
console.log(chunk);
}
```
## Documentation
To generate documentation, run the following commands:
1. Generate docs.
yarn typedoc
1. Consolidate doc files into one markdown file.
npx concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > docs/js_ts_sdk_ref.md
1. Copy `js_ts_sdk_ref.md` to MkDocs directory.
cp docs/js_ts_sdk_ref.md ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
This repository has been moved to [langchain-ai/langgraphjs](https://github.com/langchain-ai/langgraphjs/tree/main/libs/sdk).
-17
View File
@@ -1,17 +0,0 @@
/** @type {import('jest').Config} */
export default {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
};
-27
View File
@@ -1,27 +0,0 @@
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
/**
* @param {string} relativePath
* @returns {string}
*/
function abs(relativePath) {
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
}
export const config = {
internals: [/react/],
entrypoints: {
index: "index",
client: "client",
auth: "auth/index",
react: "react/index",
"react-ui": "react-ui/index",
"react-ui/server": "react-ui/server/index",
},
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
cjsDestination: "./dist",
additionalGitignorePaths: ["docs"],
abs,
};
-147
View File
@@ -1,147 +0,0 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.89",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
"scripts": {
"clean": "rm -rf dist/ dist-cjs/",
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
"prepack": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
"test": "vitest",
"typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json && typedoc src/auth/index.ts --out docs/auth --options typedoc.auth.json"
},
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15",
"p-queue": "^6.6.2",
"p-retry": "4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@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",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@tsconfig/recommended": "^1.0.2",
"@types/node": "^20.12.12",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@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",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typedoc": "^0.27.7",
"typedoc-plugin-markdown": "^4.4.2",
"typescript": "^5.4.5",
"vitest": "^3.1.3"
},
"peerDependencies": {
"@langchain/core": ">=0.2.31 <0.4.0",
"react": "^18 || ^19"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"@langchain/core": {
"optional": true
}
},
"exports": {
".": {
"types": {
"import": "./index.d.ts",
"require": "./index.d.cts",
"default": "./index.d.ts"
},
"import": "./index.js",
"require": "./index.cjs"
},
"./client": {
"types": {
"import": "./client.d.ts",
"require": "./client.d.cts",
"default": "./client.d.ts"
},
"import": "./client.js",
"require": "./client.cjs"
},
"./auth": {
"types": {
"import": "./auth.d.ts",
"require": "./auth.d.cts",
"default": "./auth.d.ts"
},
"import": "./auth.js",
"require": "./auth.cjs"
},
"./react": {
"types": {
"import": "./react.d.ts",
"require": "./react.d.cts",
"default": "./react.d.ts"
},
"import": "./react.js",
"require": "./react.cjs"
},
"./react-ui": {
"types": {
"import": "./react-ui.d.ts",
"require": "./react-ui.d.cts",
"default": "./react-ui.d.ts"
},
"import": "./react-ui.js",
"require": "./react-ui.cjs"
},
"./react-ui/server": {
"types": {
"import": "./react-ui/server.d.ts",
"require": "./react-ui/server.d.cts",
"default": "./react-ui/server.d.ts"
},
"import": "./react-ui/server.js",
"require": "./react-ui/server.cjs"
},
"./package.json": "./package.json"
},
"files": [
"dist/",
"index.cjs",
"index.js",
"index.d.ts",
"index.d.cts",
"client.cjs",
"client.js",
"client.d.ts",
"client.d.cts",
"auth.cjs",
"auth.js",
"auth.d.ts",
"auth.d.cts",
"react.cjs",
"react.js",
"react.d.ts",
"react.d.cts",
"react-ui.cjs",
"react-ui.js",
"react-ui.d.ts",
"react-ui.d.cts",
"react-ui/server.cjs",
"react-ui/server.js",
"react-ui/server.d.ts",
"react-ui/server.d.cts"
]
}
-80
View File
@@ -1,80 +0,0 @@
const HTTP_STATUS_MAPPING: { [key: number]: string } = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a Teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
};
export class HTTPException extends Error {
status: number;
headers: HeadersInit;
constructor(
status: number,
options?: { message?: string; headers?: HeadersInit; cause?: unknown },
) {
super(options?.message ?? HTTP_STATUS_MAPPING[status] ?? "Unknown error", {
cause: options?.cause,
});
this.status = status;
this.headers = options?.headers ?? {};
}
}
-46
View File
@@ -1,46 +0,0 @@
import type {
AuthenticateCallback,
AnyCallback,
CallbackEvent,
OnCallback,
BaseAuthReturn,
ToUserLike,
BaseUser,
} from "./types.js";
export class Auth<
TExtra = {},
TAuthReturn extends BaseAuthReturn = BaseAuthReturn,
TUser extends BaseUser = ToUserLike<TAuthReturn>,
> {
/**
* @internal
* @ignore
*/
"~handlerCache": {
authenticate?: AuthenticateCallback<BaseAuthReturn>;
callbacks?: Record<string, AnyCallback>;
} = {};
authenticate<T extends BaseAuthReturn>(
cb: AuthenticateCallback<T>,
): Auth<TExtra, T> {
this["~handlerCache"].authenticate = cb;
return this as unknown as Auth<TExtra, T>;
}
on<T extends CallbackEvent>(event: T, callback: OnCallback<T, TUser>): this {
this["~handlerCache"].callbacks ??= {};
const events: string[] = Array.isArray(event) ? event : [event];
for (const event of events) {
this["~handlerCache"].callbacks[event] = callback as AnyCallback;
}
return this;
}
}
export type {
Filters as AuthFilters,
EventValueMap as AuthEventValueMap,
} from "./types.js";
export { HTTPException } from "./error.js";
-411
View File
@@ -1,411 +0,0 @@
type Maybe<T> = T | null | undefined;
type PromiseMaybe<T> = Promise<T> | T;
interface AssistantConfig {
tags?: Maybe<string[]>;
recursion_limit?: Maybe<number>;
configurable?: Maybe<{
thread_id?: Maybe<string>;
thread_ts?: Maybe<string>;
[key: string]: unknown;
}>;
}
/**
* @inline
*/
interface AssistantCreate {
assistant_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
config?: Maybe<AssistantConfig>;
if_exists?: Maybe<"raise" | "do_nothing">;
name?: Maybe<string>;
graph_id: string;
}
/**
* @inline
*/
interface AssistantRead {
assistant_id: string;
metadata?: Maybe<Record<string, unknown>>;
}
/**
* @inline
*/
interface AssistantUpdate {
assistant_id: string;
metadata?: Maybe<Record<string, unknown>>;
config?: Maybe<AssistantConfig>;
graph_id?: Maybe<string>;
name?: Maybe<string>;
version?: Maybe<number>;
}
/**
* @inline
*/
interface AssistantDelete {
assistant_id: string;
}
/**
* @inline
*/
interface AssistantSearch {
graph_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
limit?: Maybe<number>;
offset?: Maybe<number>;
}
/**
* @inline
*/
interface ThreadCreate {
thread_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
if_exists?: Maybe<"raise" | "do_nothing">;
}
/**
* @inline
*/
interface ThreadRead {
thread_id?: Maybe<string>;
}
/**
* @inline
*/
interface ThreadUpdate {
thread_id?: Maybe<string>;
metadata?: Maybe<Record<string, unknown>>;
action?: Maybe<"interrupt" | "rollback">;
}
/**
* @inline
*/
interface ThreadDelete {
thread_id?: Maybe<string>;
run_id?: Maybe<string>;
}
/**
* @inline
*/
interface ThreadSearch {
thread_id?: Maybe<string>;
status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>;
metadata?: Maybe<Record<string, unknown>>;
values?: Maybe<Record<string, unknown>>;
limit?: Maybe<number>;
offset?: Maybe<number>;
}
/**
* @inline
*/
interface CronCreate {
payload?: Maybe<Record<string, unknown>>;
schedule: string;
cron_id?: Maybe<string>;
thread_id?: Maybe<string>;
user_id?: Maybe<string>;
end_time?: Maybe<string>;
}
/**
* @inline
*/
interface CronRead {
cron_id: string;
}
/**
* @inline
*/
interface CronUpdate {
cron_id: string;
payload?: Maybe<Record<string, unknown>>;
schedule?: Maybe<string>;
}
/**
* @inline
*/
interface CronDelete {
cron_id: string;
}
/**
* @inline
*/
interface CronSearch {
assistant_id?: Maybe<string>;
thread_id?: Maybe<string>;
limit?: Maybe<number>;
offset?: Maybe<number>;
}
/**
* @inline
*/
interface StorePut {
namespace: string[];
key: string;
value: Record<string, unknown>;
}
/**
* @inline
*/
interface StoreGet {
namespace: Maybe<string[]>;
key: string;
}
/**
* @inline
*/
interface StoreSearch {
namespace?: Maybe<string[]>;
filter?: Maybe<Record<string, unknown>>;
limit?: Maybe<number>;
offset?: Maybe<number>;
query?: Maybe<string>;
}
/**
* @inline
*/
interface StoreListNamespaces {
namespace?: Maybe<string[]>;
suffix?: Maybe<string[]>;
max_depth?: Maybe<number>;
limit?: Maybe<number>;
offset?: Maybe<number>;
}
/**
* @inline
*/
interface StoreDelete {
namespace?: Maybe<string[]>;
key: string;
}
/**
* @inline
*/
interface RunsCreate {
thread_id?: Maybe<string>;
assistant_id: string;
run_id: string;
status: Maybe<
"pending" | "running" | "error" | "success" | "timeout" | "interrupted"
>;
metadata?: Maybe<Record<string, unknown>>;
prevent_insert_if_inflight?: Maybe<boolean>;
multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">;
if_not_exists?: Maybe<"reject" | "create">;
after_seconds?: Maybe<number>;
kwargs: Record<string, unknown>;
}
export interface EventValueMap {
["threads:create"]: ThreadCreate;
["threads:read"]: ThreadRead;
["threads:update"]: ThreadUpdate;
["threads:delete"]: ThreadDelete;
["threads:search"]: ThreadSearch;
["threads:create_run"]: RunsCreate;
["assistants:create"]: AssistantCreate;
["assistants:read"]: AssistantRead;
["assistants:update"]: AssistantUpdate;
["assistants:delete"]: AssistantDelete;
["assistants:search"]: AssistantSearch;
["crons:create"]: CronCreate;
["crons:read"]: CronRead;
["crons:update"]: CronUpdate;
["crons:delete"]: CronDelete;
["crons:search"]: CronSearch;
["store:put"]: StorePut;
["store:get"]: StoreGet;
["store:search"]: StoreSearch;
["store:list_namespaces"]: StoreListNamespaces;
["store:delete"]: StoreDelete;
}
interface ResourceType {
threads:
| "threads:create"
| "threads:read"
| "threads:update"
| "threads:delete"
| "threads:search"
| "threads:create_run";
assistants:
| "assistants:create"
| "assistants:read"
| "assistants:update"
| "assistants:delete"
| "assistants:search";
crons:
| "crons:create"
| "crons:read"
| "crons:update"
| "crons:delete"
| "crons:search";
store:
| "store:put"
| "store:get"
| "store:search"
| "store:list_namespaces"
| "store:delete";
}
interface ActionType {
"*:create": "threads:create" | "assistants:create" | "crons:create";
"*:read": "threads:read" | "assistants:read" | "crons:read";
"*:update": "threads:update" | "assistants:update" | "crons:update";
"*:delete":
| "threads:delete"
| "assistants:delete"
| "crons:delete"
| "store:delete";
"*:search":
| "threads:search"
| "assistants:search"
| "crons:search"
| "store:search";
"*:create_run": "threads:create_run";
"*:put": "store:put";
"*:get": "store:get";
"*:list_namespaces": "store:list_namespaces";
}
export type BaseAuthReturn =
| {
is_authenticated?: boolean;
display_name?: string;
identity: string;
permissions: string[];
}
| string;
export interface BaseUser {
is_authenticated: boolean;
display_name: string;
identity: string;
permissions: string[];
}
export type ToUserLike<T extends BaseAuthReturn> = T extends string
? {
is_authenticated: boolean;
display_name: string;
identity: string;
permissions: string[];
}
: Omit<T, "is_authenticated" | "display_name"> & {
is_authenticated: boolean;
display_name: string;
};
type CallbackParameter<
Event extends string = string,
Resource extends string = string,
Action extends string = string,
Value extends unknown = unknown,
TUser extends BaseUser = BaseUser,
> = {
event: Event;
resource: Resource;
action: Action;
value: Value;
user: TUser;
permissions: string[];
};
type ContextMap = {
[EventType in keyof EventValueMap]: CallbackParameter<
EventType,
EventType extends `${infer Resource}:${string}` ? Resource : never,
EventType extends `${string}:${infer Action}` ? Action : never,
EventValueMap[EventType],
BaseUser
>;
};
type ActionCallbackParameter<
T extends keyof ActionType,
TUser extends BaseUser = BaseUser,
> = ContextMap[ActionType[T]] & { user: TUser };
type AuthCallbackParameter<
T extends keyof EventValueMap,
TUser extends BaseUser = BaseUser,
> = ContextMap[T] & { user: TUser };
type ResourceCallbackParameter<
T extends keyof ResourceType,
TUser extends BaseUser = BaseUser,
> = ContextMap[ResourceType[T]] & { user: TUser };
export type Filters<TKey extends string | number | symbol> = {
[key in TKey]: string | { [op in "$contains" | "$eq"]?: string };
};
export interface AuthenticateCallback<T extends BaseAuthReturn> {
(request: Request): PromiseMaybe<T>;
}
type OnKey = keyof ResourceType | keyof ActionType | keyof EventValueMap;
type OnSingleParameter<
T extends OnKey,
TUser extends BaseUser = BaseUser,
> = T extends keyof ResourceType
? ResourceCallbackParameter<T, TUser>
: T extends keyof ActionType
? ActionCallbackParameter<T, TUser>
: T extends keyof EventValueMap
? AuthCallbackParameter<T, TUser>
: never;
type OnParameter<
T extends "*" | OnKey | OnKey[],
TUser extends BaseUser = BaseUser,
> = T extends OnKey[]
? OnSingleParameter<T[number], TUser>
: T extends "*"
? AuthCallbackParameter<keyof EventValueMap, TUser>
: T extends OnKey
? OnSingleParameter<T, TUser>
: never;
export type AnyCallback = (
request: CallbackParameter,
) => void | boolean | Filters<string>;
export type CallbackEvent = "*" | OnKey | OnKey[];
export type OnCallback<
T extends CallbackEvent,
TUser extends BaseUser = BaseUser,
TMetadata extends Record<string, unknown> = Record<string, unknown>,
> = (
request: OnParameter<T, TUser>,
) => void | boolean | Filters<keyof TMetadata>;
File diff suppressed because it is too large Load Diff
-56
View File
@@ -1,56 +0,0 @@
export { Client, getApiKey } from "./client.js";
export type { ClientConfig, RequestHook } from "./client.js";
export type {
Assistant,
AssistantBase,
AssistantGraph,
AssistantVersion,
Checkpoint,
Config,
Cron,
CronCreateForThreadResponse,
CronCreateResponse,
DefaultValues,
GraphSchema,
Interrupt,
Item,
ListNamespaceResponse,
Metadata,
Run,
SearchItem,
SearchItemsResponse,
Thread,
ThreadState,
ThreadStatus,
ThreadTask,
} from "./schema.js";
export { overrideFetchImplementation } from "./singletons/fetch.js";
export type {
Command,
OnConflictBehavior,
RunsInvokePayload,
} from "./types.js";
export type {
AIMessage,
FunctionMessage,
HumanMessage,
Message,
RemoveMessage,
SystemMessage,
ToolMessage,
} from "./types.messages.js";
export type {
CustomStreamEvent,
DebugStreamEvent,
ErrorStreamEvent,
EventsStreamEvent,
FeedbackStreamEvent,
MessagesStreamEvent,
MessagesTupleStreamEvent,
MetadataStreamEvent,
StreamMode,
UpdatesStreamEvent,
ValuesStreamEvent,
} from "./types.stream.js";
-279
View File
@@ -1,279 +0,0 @@
"use client";
import { useStream } from "../react/index.js";
import type { UIMessage } from "./types.js";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as JsxRuntime from "react/jsx-runtime";
import type { UseStream } from "../react/stream.js";
const UseStreamContext = React.createContext<{
stream: ReturnType<typeof useStream>;
meta: unknown;
}>(null!);
type BagTemplate = {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
MetaType?: unknown;
};
type GetMetaType<Bag extends BagTemplate> = Bag extends { MetaType: unknown }
? Bag["MetaType"]
: unknown;
interface UseStreamContext<
StateType extends Record<string, unknown> = Record<string, unknown>,
Bag extends BagTemplate = BagTemplate,
> extends UseStream<StateType, Bag> {
meta?: GetMetaType<Bag>;
}
export function useStreamContext<
StateType extends Record<string, unknown> = Record<string, unknown>,
Bag extends {
ConfigurableType?: Record<string, unknown>;
InterruptType?: unknown;
CustomEventType?: unknown;
UpdateType?: unknown;
MetaType?: unknown;
} = BagTemplate,
>(): UseStreamContext<StateType, Bag> {
const ctx = React.useContext(UseStreamContext);
if (!ctx) {
throw new Error(
"useStreamContext must be used within a LoadExternalComponent",
);
}
return new Proxy(ctx, {
get(target, prop: keyof UseStreamContext<StateType, Bag>) {
if (prop === "meta") return target.meta;
return target.stream[prop];
},
}) as unknown as UseStreamContext<StateType, Bag>;
}
interface ComponentTarget {
comp: React.FunctionComponent | React.ComponentClass;
target: HTMLElement;
}
class ComponentStore {
private cache: Record<string, ComponentTarget> = {};
private boundCache: Record<
string,
{
subscribe: (onStoreChange: () => void) => () => void;
getSnapshot: () => ComponentTarget | undefined;
}
> = {};
private callbacks: Record<
string,
((
comp: React.FunctionComponent | React.ComponentClass,
el: HTMLElement,
) => void)[]
> = {};
respond(
shadowRootId: string,
comp: React.FunctionComponent | React.ComponentClass,
targetElement: HTMLElement,
) {
this.cache[shadowRootId] = { comp, target: targetElement };
this.callbacks[shadowRootId]?.forEach((c) => c(comp, targetElement));
}
getBoundStore(shadowRootId: string) {
this.boundCache[shadowRootId] ??= {
subscribe: (onStoreChange: () => void) => {
this.callbacks[shadowRootId] ??= [];
this.callbacks[shadowRootId].push(onStoreChange);
return () => {
this.callbacks[shadowRootId] = this.callbacks[shadowRootId].filter(
(c) => c !== onStoreChange,
);
};
},
getSnapshot: () => this.cache[shadowRootId],
};
return this.boundCache[shadowRootId];
}
}
const COMPONENT_STORE = new ComponentStore();
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
const REQUIRE_EXTRA_SYMBOL = Symbol.for("LGUI_REQUIRE_EXTRA");
interface LoadExternalComponentProps
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
/** Stream of the assistant */
stream: ReturnType<typeof useStream>;
/** Namespace of UI components. Defaults to assistant ID. */
namespace?: string;
/** UI message to be rendered */
message: UIMessage;
/** Additional context to be passed to the child component */
meta?: unknown;
/** Fallback to be rendered when the component is loading */
fallback?: React.ReactNode | Record<string, React.ReactNode>;
/**
* Map of components that can be rendered directly without fetching the UI code
* from the server.
*/
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
}
const isIterable = (value: unknown): value is Iterable<unknown> =>
value != null && typeof value === "object" && Symbol.iterator in value;
const isPromise = (value: unknown): value is Promise<unknown> =>
value != null &&
typeof value === "object" &&
"then" in value &&
typeof value.then === "function";
const isReactNode = (value: unknown): value is React.ReactNode => {
if (React.isValidElement(value)) return true;
if (value == null) return true;
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "bigint" ||
typeof value === "boolean"
) {
return true;
}
if (isIterable(value)) return true;
if (isPromise(value)) return true;
return false;
};
export function LoadExternalComponent({
stream,
namespace,
message,
meta,
fallback,
components,
...props
}: LoadExternalComponentProps) {
const ref = React.useRef<HTMLDivElement>(null);
const id = React.useId();
const shadowRootId = `child-shadow-${id}`;
const store = React.useMemo(
() => COMPONENT_STORE.getBoundStore(shadowRootId),
[shadowRootId],
);
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
const clientComponent = components?.[message.name];
const hasClientComponent = clientComponent != null;
const fallbackComponent = isReactNode(fallback)
? fallback
: typeof fallback === "object" && fallback != null
? fallback?.[message.name]
: null;
const uiNamespace = namespace ?? stream.assistantId;
const uiClient = stream.client["~ui"];
React.useEffect(() => {
if (hasClientComponent) return;
uiClient.getComponent(uiNamespace, message.name).then((html) => {
const dom = ref.current;
if (!dom) return;
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
const fragment = document
.createRange()
.createContextualFragment(
html.replace("{{shadowRootId}}", shadowRootId),
);
root.appendChild(fragment);
});
}, [uiClient, uiNamespace, message.name, shadowRootId, hasClientComponent]);
if (hasClientComponent) {
return (
<UseStreamContext.Provider value={{ stream, meta }}>
{React.createElement(clientComponent, message.props)}
</UseStreamContext.Provider>
);
}
return (
<>
<div id={shadowRootId} ref={ref} {...props} />
<UseStreamContext.Provider value={{ stream, meta }}>
{state?.target != null
? ReactDOM.createPortal(
React.createElement(state.comp, message.props),
state.target,
)
: fallbackComponent}
</UseStreamContext.Provider>
</>
);
}
declare global {
interface Window {
[EXT_STORE_SYMBOL]: ComponentStore;
[REQUIRE_SYMBOL]: (name: string) => unknown;
[REQUIRE_EXTRA_SYMBOL]: Record<string, unknown>;
}
}
export function experimental_loadShare(name: string, module: unknown) {
if (typeof window === "undefined") return;
window[REQUIRE_EXTRA_SYMBOL] ??= {};
window[REQUIRE_EXTRA_SYMBOL][name] = module;
}
export function bootstrapUiContext() {
if (typeof window === "undefined") {
return;
}
window[EXT_STORE_SYMBOL] = COMPONENT_STORE;
window[REQUIRE_SYMBOL] = (name: string) => {
if (name === "react") return React;
if (name === "react-dom") return ReactDOM;
if (name === "react/jsx-runtime") return JsxRuntime;
if (name === "@langchain/langgraph-sdk/react") return { useStream };
if (name === "@langchain/langgraph-sdk/react-ui") {
return {
useStreamContext,
LoadExternalComponent: () => {
throw new Error("Nesting LoadExternalComponent is not supported");
},
};
}
if (
window[REQUIRE_EXTRA_SYMBOL] != null &&
typeof window[REQUIRE_EXTRA_SYMBOL] === "object" &&
name in window[REQUIRE_EXTRA_SYMBOL]
) {
return window[REQUIRE_EXTRA_SYMBOL][name];
}
throw new Error(`Unknown module...: ${name}`);
};
}
-15
View File
@@ -1,15 +0,0 @@
import { bootstrapUiContext } from "./client.js";
bootstrapUiContext();
export {
useStreamContext,
LoadExternalComponent,
experimental_loadShare,
} from "./client.js";
export {
uiMessageReducer,
isUIMessage,
isRemoveUIMessage,
type UIMessage,
type RemoveUIMessage,
} from "./types.js";
-6
View File
@@ -1,6 +0,0 @@
export { typedUi } from "./server.js";
export {
uiMessageReducer,
type UIMessage,
type RemoveUIMessage,
} from "../types.js";
-99
View File
@@ -1,99 +0,0 @@
import { v4 as uuidv4 } from "uuid";
import type { ComponentPropsWithoutRef, ElementType } from "react";
import type { RemoveUIMessage, UIMessage } from "../types.js";
interface MessageLike {
id?: string;
}
/**
* Helper to send and persist UI messages. Accepts a map of component names to React components
* as type argument to provide type safety. Will also write to the `options?.stateKey` state.
*
* @param config LangGraphRunnableConfig
* @param options
* @returns
*/
export const typedUi = <Decl extends Record<string, ElementType>>(
config: {
writer?: (chunk: unknown) => void;
runId?: string;
metadata?: Record<string, unknown>;
tags?: string[];
runName?: string;
configurable?: {
__pregel_send?: (writes_: [string, unknown][]) => void;
[key: string]: unknown;
};
},
options?: {
/** The key to write the UI messages to. Defaults to `ui`. */
stateKey?: string;
},
) => {
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
let items: (UIMessage | RemoveUIMessage)[] = [];
const stateKey = options?.stateKey ?? "ui";
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
if (!runId) throw new Error("run_id is required");
function handlePush<K extends keyof PropMap & string>(
message: {
id?: string;
name: K;
props: PropMap[K];
metadata?: Record<string, unknown>;
},
options?: { message?: MessageLike; merge?: boolean },
): UIMessage<K, PropMap[K]>;
function handlePush<K extends keyof PropMap & string>(
message: {
id?: string;
name: K;
props: Partial<PropMap[K]>;
metadata?: Record<string, unknown>;
},
options: { message?: MessageLike; merge: true },
): UIMessage<K, Partial<PropMap[K]>>;
function handlePush<K extends keyof PropMap & string>(
message: {
id?: string;
name: K;
props: PropMap[K] | Partial<PropMap[K]>;
metadata?: Record<string, unknown>;
},
options?: { message?: MessageLike; merge?: boolean },
): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {
const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {
type: "ui" as const,
id: message?.id ?? uuidv4(),
name: message?.name,
props: message?.props,
metadata: {
merge: options?.merge || undefined,
run_id: runId,
tags: config.tags,
name: config.runName,
...message?.metadata,
...(options?.message ? { message_id: options.message.id } : null),
},
};
items.push(evt);
config.writer?.(evt);
config.configurable?.__pregel_send?.([[stateKey, evt]]);
return evt;
}
const handleDelete = (id: string): RemoveUIMessage => {
const evt: RemoveUIMessage = { type: "remove-ui", id };
items.push(evt);
config.writer?.(evt);
config.configurable?.__pregel_send?.([[stateKey, evt]]);
return evt;
};
return { push: handlePush, delete: handleDelete, items };
};
-66
View File
@@ -1,66 +0,0 @@
export interface UIMessage<
TName extends string = string,
TProps extends Record<string, unknown> = Record<string, unknown>,
> {
type: "ui";
id: string;
name: TName;
props: TProps;
metadata?: {
merge?: boolean;
run_id?: string;
name?: string;
tags?: string[];
message_id?: string;
[key: string]: unknown;
};
}
export interface RemoveUIMessage {
type: "remove-ui";
id: string;
}
export function isUIMessage(message: unknown): message is UIMessage {
if (typeof message !== "object" || message == null) return false;
if (!("type" in message)) return false;
return message.type === "ui";
}
export function isRemoveUIMessage(
message: unknown,
): message is RemoveUIMessage {
if (typeof message !== "object" || message == null) return false;
if (!("type" in message)) return false;
return message.type === "remove-ui";
}
export function uiMessageReducer(
state: UIMessage[],
update: UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[],
) {
const events = Array.isArray(update) ? update : [update];
let newState = state.slice();
for (const event of events) {
if (event.type === "remove-ui") {
newState = newState.filter((ui) => ui.id !== event.id);
continue;
}
const index = state.findIndex((ui) => ui.id === event.id);
if (index !== -1) {
newState[index] =
typeof event.metadata === "object" &&
event.metadata != null &&
event.metadata.merge
? { ...event, props: { ...state[index].props, ...event.props } }
: event;
} else {
newState.push(event);
}
}
return newState;
}
-102
View File
@@ -1,102 +0,0 @@
import { ThreadState } from "../schema.js";
interface Node<StateType = any> {
type: "node";
value: ThreadState<StateType>;
path: string[];
}
interface Fork<StateType = any> {
type: "fork";
items: Array<Sequence<StateType>>;
}
interface Sequence<StateType = any> {
type: "sequence";
items: Array<Node<StateType> | Fork<StateType>>;
}
interface ValidFork<StateType = any> {
type: "fork";
items: Array<ValidSequence<StateType>>;
}
interface ValidSequence<StateType = any> {
type: "sequence";
items: [Node<StateType>, ...(Node<StateType> | ValidFork<StateType>)[]];
}
// forks
export type CheckpointBranchPath = string[];
export type MessageBranch = {
current: CheckpointBranchPath;
options: CheckpointBranchPath[];
};
export function DebugSegmentsView(props: {
sequence: ValidSequence<ThreadState>;
}) {
const concatContent = (value: ThreadState<any>) => {
let content;
try {
content = value.values?.messages?.at(-1)?.content ?? "";
} catch {
content = JSON.stringify(value.values);
}
content = content.replace(/(\n|\r\n)/g, "");
if (content.length <= 23) return content;
return `${content.slice(0, 10)}...${content.slice(-10)}`;
};
return (
<div>
{props.sequence.items.map((item, index) => {
if (item.type === "fork") {
return (
<div key={index}>
{item.items.map((fork, idx) => {
const [first] = fork.items;
return (
<details key={idx}>
<summary>
Fork{" "}
<span className="font-mono">
...{first.path.at(-1)?.slice(-4)}
</span>
</summary>
<div className="ml-4">
<DebugSegmentsView sequence={fork} />
</div>
</details>
);
})}
</div>
);
}
if (item.type === "node") {
return (
<div key={index} className="flex items-center gap-2">
<pre>
({item.value.metadata?.step}) ...
{item.value.checkpoint.checkpoint_id?.slice(-4)} (
{item.value.metadata?.source}): {concatContent(item.value)}
</pre>
<button
type="button"
className="border rounded-sm text-sm py-0.5 px-1 text-muted-foreground"
onClick={() => console.log(item.path, item.value)}
>
console.log
</button>
</div>
);
}
return null;
})}
</div>
);
}
-6
View File
@@ -1,6 +0,0 @@
export {
useStream,
type MessageMetadata,
type UseStream,
type UseStreamOptions,
} from "./stream.js";
File diff suppressed because it is too large Load Diff
-334
View File
@@ -1,334 +0,0 @@
import type { JSONSchema7 } from "json-schema";
type Optional<T> = T | null | undefined;
export type RunStatus =
| "pending"
| "running"
| "error"
| "success"
| "timeout"
| "interrupted";
export type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
export type CancelAction = "interrupt" | "rollback";
export type Config = {
/**
* Tags for this call and any sub-calls (eg. a Chain calling an LLM).
* You can use these to filter calls.
*/
tags?: string[];
/**
* Maximum number of times a call can recurse.
* If not provided, defaults to 25.
*/
recursion_limit?: number;
/**
* Runtime values for attributes previously made configurable on this Runnable.
*/
configurable?: {
/**
* ID of the thread
*/
thread_id?: Optional<string>;
/**
* Timestamp of the state checkpoint
*/
checkpoint_id?: Optional<string>;
[key: string]: unknown;
};
};
export interface GraphSchema {
/**
* The ID of the graph.
*/
graph_id: string;
/**
* The schema for the input state.
* Missing if unable to generate JSON schema from graph.
*/
input_schema?: JSONSchema7 | null | undefined;
/**
* The schema for the output state.
* Missing if unable to generate JSON schema from graph.
*/
output_schema?: JSONSchema7 | null | undefined;
/**
* The schema for the graph state.
* Missing if unable to generate JSON schema from graph.
*/
state_schema?: JSONSchema7 | null | undefined;
/**
* The schema for the graph config.
* Missing if unable to generate JSON schema from graph.
*/
config_schema?: JSONSchema7 | null | undefined;
}
export type Subgraphs = Record<string, GraphSchema>;
export type Metadata = Optional<{
source?: "input" | "loop" | "update" | (string & {});
step?: number;
writes?: Record<string, unknown> | null;
parents?: Record<string, string>;
[key: string]: unknown;
}>;
export interface AssistantBase {
/** The ID of the assistant. */
assistant_id: string;
/** The ID of the graph. */
graph_id: string;
/** The assistant config. */
config: Config;
/** The time the assistant was created. */
created_at: string;
/** The assistant metadata. */
metadata: Metadata;
/** The version of the assistant. */
version: number;
/** The name of the assistant */
name: string;
/** The description of the assistant */
description?: string;
}
export interface AssistantVersion extends AssistantBase {}
export interface Assistant extends AssistantBase {
/** The last time the assistant was updated. */
updated_at: string;
}
export interface AssistantGraph {
nodes: Array<{
id: string | number;
name?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: Record<string, any> | string;
metadata?: unknown;
}>;
edges: Array<{
source: string;
target: string;
data?: string;
conditional?: boolean;
}>;
}
/**
* An interrupt thrown inside a thread.
*/
export interface Interrupt<TValue = unknown> {
value?: TValue;
when: "during" | (string & {});
resumable?: boolean;
ns?: string[];
}
export interface Thread<ValuesType = DefaultValues> {
/** The ID of the thread. */
thread_id: string;
/** The time the thread was created. */
created_at: string;
/** The last time the thread was updated. */
updated_at: string;
/** The thread metadata. */
metadata: Metadata;
/** The status of the thread */
status: ThreadStatus;
/** The current state of the thread. */
values: ValuesType;
/** Interrupts which were thrown in this thread */
interrupts: Record<string, Array<Interrupt>>;
}
export interface Cron {
/** The ID of the cron */
cron_id: string;
/** The ID of the assistant */
assistant_id: string;
/** The ID of the thread */
thread_id: Optional<string>;
/** The end date to stop running the cron. */
end_time: Optional<string>;
/** The schedule to run, cron format. */
schedule: string;
/** The time the cron was created. */
created_at: string;
/** The last time the cron was updated. */
updated_at: string;
/** The run payload to use for creating new run. */
payload: Record<string, unknown>;
/** The user ID of the cron */
user_id: Optional<string>;
/** The next run date of the cron */
next_run_date: Optional<string>;
/** The metadata of the cron */
metadata: Record<string, unknown>;
}
export type DefaultValues = Record<string, unknown>[] | Record<string, unknown>;
export interface ThreadState<ValuesType = DefaultValues> {
/** The state values */
values: ValuesType;
/** The next nodes to execute. If empty, the thread is done until new input is received */
next: string[];
/** Checkpoint of the thread state */
checkpoint: Checkpoint;
/** Metadata for this state */
metadata: Metadata;
/** Time of state creation */
created_at: Optional<string>;
/** The parent checkpoint. If missing, this is the root checkpoint */
parent_checkpoint: Optional<Checkpoint>;
/** Tasks to execute in this step. If already attempted, may contain an error */
tasks: Array<ThreadTask>;
}
export interface ThreadTask {
id: string;
name: string;
result?: unknown;
error: Optional<string>;
interrupts: Array<Interrupt>;
checkpoint: Optional<Checkpoint>;
state: Optional<ThreadState>;
}
export interface Run {
/** The ID of the run */
run_id: string;
/** The ID of the thread */
thread_id: string;
/** The assistant that wwas used for this run */
assistant_id: string;
/** The time the run was created */
created_at: string;
/** The last time the run was updated */
updated_at: string;
/** The status of the run. */
status: RunStatus;
/** Run metadata */
metadata: Metadata;
/** Strategy to handle concurrent runs on the same thread */
multitask_strategy: Optional<MultitaskStrategy>;
}
export type Checkpoint = {
thread_id: string;
checkpoint_ns: string;
checkpoint_id: Optional<string>;
checkpoint_map: Optional<Record<string, unknown>>;
};
export interface ListNamespaceResponse {
namespaces: string[][];
}
export interface Item {
namespace: string[];
key: string;
value: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface SearchItem extends Item {
score?: number;
}
export interface SearchItemsResponse {
items: SearchItem[];
}
export interface CronCreateResponse {
cron_id: string;
assistant_id: string;
thread_id: string | undefined;
user_id: string;
payload: Record<string, unknown>;
schedule: string;
next_run_date: string;
end_time: string | undefined;
created_at: string;
updated_at: string;
metadata: Metadata;
}
export interface CronCreateForThreadResponse
extends Omit<CronCreateResponse, "thread_id"> {
thread_id: string;
}
export type AssistantSortBy =
| "assistant_id"
| "graph_id"
| "name"
| "created_at"
| "updated_at";
export type ThreadSortBy = "thread_id" | "status" | "created_at" | "updated_at";
export type CronSortBy =
| "cron_id"
| "assistant_id"
| "thread_id"
| "created_at"
| "updated_at"
| "next_run_date";
export type SortOrder = "asc" | "desc";
-29
View File
@@ -1,29 +0,0 @@
// Wrap the default fetch call due to issues with illegal invocations
// in some environments:
// https://stackoverflow.com/questions/69876859/why-does-bind-fix-failed-to-execute-fetch-on-window-illegal-invocation-err
// @ts-expect-error Broad typing to support a range of fetch implementations
const DEFAULT_FETCH_IMPLEMENTATION = (...args: any[]) => fetch(...args);
const LANGSMITH_FETCH_IMPLEMENTATION_KEY = Symbol.for(
"lg:fetch_implementation",
);
/**
* Overrides the fetch implementation used for LangSmith calls.
* You should use this if you need to use an implementation of fetch
* other than the default global (e.g. for dealing with proxies).
* @param fetch The new fetch function to use.
*/
export const overrideFetchImplementation = (fetch: (...args: any[]) => any) => {
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] = fetch;
};
/**
* @internal
*/
export const _getFetchImplementation: () => (...args: any[]) => any = () => {
return (
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] ??
DEFAULT_FETCH_IMPLEMENTATION
);
};
-201
View File
@@ -1,201 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Client } from "../client.js";
import { overrideFetchImplementation } from "../singletons/fetch.js";
describe.each([["global"], ["mocked"]])(
"Client uses %s fetch",
(description: string) => {
let globalFetchMock: ReturnType<typeof vi.fn>;
let overriddenFetch: ReturnType<typeof vi.fn>;
let expectedFetchMock: ReturnType<typeof vi.fn>;
let unexpectedFetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
globalFetchMock = vi.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
batch_ingest_config: {
use_multipart_endpoint: true,
},
}),
text: () => Promise.resolve(""),
headers: new Headers({}),
}),
);
overriddenFetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
batch_ingest_config: {
use_multipart_endpoint: true,
},
}),
text: () => Promise.resolve(""),
headers: new Headers({}),
}),
);
expectedFetchMock =
description === "mocked" ? overriddenFetch : globalFetchMock;
unexpectedFetchMock =
description === "mocked" ? globalFetchMock : overriddenFetch;
if (description === "mocked") {
overrideFetchImplementation(overriddenFetch);
} else {
overrideFetchImplementation(globalFetchMock);
}
// Mock global fetch
(globalThis as any).fetch = globalFetchMock;
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("createRuns", () => {
it("should create an example with the given input and generation", async () => {
const client = new Client({ apiKey: "test-api-key" });
const thread = await client.threads.create();
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
expect(unexpectedFetchMock).not.toHaveBeenCalled();
vi.clearAllMocks(); // Clear all mocks before the next operation
// Then clear & run the function
await client.runs.create(thread.thread_id, "somegraph", {
input: { foo: "bar" },
});
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
expect(unexpectedFetchMock).not.toHaveBeenCalled();
});
});
describe("header coalescing", () => {
it("should properly merge headers with conflicting name casing", async () => {
const client = new Client({ apiKey: "test-api-key" });
await (client.threads as any).fetch("/test", {
headers: { "X-Api-Key": "custom-value" },
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "custom-value",
}),
}),
);
});
it("should properly merge headers from multiple sources", async () => {
const client = new Client({
apiKey: "test-api-key",
defaultHeaders: {
"x-default": "default-value",
"x-override": "default-value",
},
});
await (client.threads as any).fetch("/test", {
headers: {
"x-custom": "custom-value",
"x-override": "custom-value",
},
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-default": "default-value",
"x-custom": "custom-value",
"x-override": "custom-value",
}),
}),
);
vi.clearAllMocks();
// Test with null/undefined values
await (client.threads as any).fetch("/test", {
headers: {
"x-null": null,
"x-undefined": undefined,
"x-empty": "",
},
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-default": "default-value",
}),
}),
);
expect(expectedFetchMock).not.toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-null": null,
"x-undefined": undefined,
}),
}),
);
});
it("should handle Headers object input", async () => {
const client = new Client({ apiKey: "test-api-key" });
const headers = new Headers();
headers.append("x-custom", "custom-value");
headers.append("x-multi", "value1");
headers.append("x-multi", "value2");
await (client.threads as any).fetch("/test", { headers });
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-custom": "custom-value",
"x-multi": "value1, value2",
}),
}),
);
});
it("should handle array of header tuples", async () => {
const client = new Client({
apiKey: "test-api-key",
defaultHeaders: {
"x-custom": "custom-value",
},
});
const headers = [
["x-multi", "value1"],
["x-multi", "value2"],
];
await (client.threads as any).fetch("/test", { headers });
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-custom": "custom-value",
"x-multi": "value1, value2",
}),
}),
);
});
});
},
);
-183
View File
@@ -1,183 +0,0 @@
import { describe, test, expect } from "vitest";
import { Readable } from "node:stream";
import { IterableReadableStream } from "../utils/stream.js";
import { BytesLineDecoder, SSEDecoder } from "../utils/sse.js";
const gather = async <T>(stream: ReadableStream<T>): Promise<T[]> => {
const results: T[] = [];
const iterator = IterableReadableStream.fromReadableStream(stream);
for await (const chunk of iterator) results.push(chunk);
return results;
};
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
describe("BytesLineDecoder", () => {
const createStream = (chunks: Uint8Array[]) => {
return Readable.toWeb(Readable.from(chunks)) as ReadableStream<Uint8Array>;
};
test("handles single line with newline", async () => {
const input = createStream([textEncoder.encode("hello\n")]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(textDecoder.decode(results[0])).toBe("hello");
});
test("handles multiple lines", async () => {
const input = createStream([textEncoder.encode("line1\nline2\nline3\n")]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(3);
expect(textDecoder.decode(results[0])).toBe("line1");
expect(textDecoder.decode(results[1])).toBe("line2");
expect(textDecoder.decode(results[2])).toBe("line3");
});
test("handles split chunks", async () => {
const input = createStream([
textEncoder.encode("li"),
textEncoder.encode("ne1\nli"),
textEncoder.encode("ne2\n"),
]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
expect(textDecoder.decode(results[0])).toBe("line1");
expect(textDecoder.decode(results[1])).toBe("line2");
});
test("handles CR LF line endings", async () => {
const input = createStream([textEncoder.encode("line1\r\nline2\r\n")]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
expect(textDecoder.decode(results[0])).toBe("line1");
expect(textDecoder.decode(results[1])).toBe("line2");
});
test("handles split CR LF", async () => {
const input = createStream([
textEncoder.encode("line1\r"),
textEncoder.encode("\nline2\r\n"),
]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
expect(textDecoder.decode(results[0])).toBe("line1");
expect(textDecoder.decode(results[1])).toBe("line2");
});
test("handles stale line", async () => {
const input = createStream([textEncoder.encode("hello")]);
const decoded = input.pipeThrough(BytesLineDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(textDecoder.decode(results[0])).toBe("hello");
});
});
describe("SSEDecoder", () => {
const createStream = (lines: string[]) => {
return Readable.toWeb(
Readable.from(lines.map((line) => textEncoder.encode(line))),
) as ReadableStream<Uint8Array>;
};
test("decodes simple event", async () => {
const input = createStream([
"event: test\n",
'data: {"message": "hello"}\n',
"\n",
]);
const decoded = input
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(results[0]).toEqual({
event: "test",
data: { message: "hello" },
});
});
test("ignores comments", async () => {
const input = createStream([
": this is a comment\n",
"event: test\n",
'data: {"message": "hello"}\n',
]);
const decoded = input
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(results[0]).toEqual({
event: "test",
data: { message: "hello" },
});
});
test("handles multiple events", async () => {
const input = createStream([
"event: test1\n",
'data: {"message": "hello"}\n',
"\n",
"event: test2\n",
'data: {"message": "world"}\n',
"\n",
]);
const decoded = input
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(2);
expect(results[0]).toEqual({
event: "test1",
data: { message: "hello" },
});
expect(results[1]).toEqual({
event: "test2",
data: { message: "world" },
});
});
test("end event without data", async () => {
const input = createStream(["event: test\n"]);
const decoded = input
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(results[0]).toEqual({
event: "test",
data: null,
});
});
test("end event without newline", async () => {
const input = createStream(["event: end"]);
const decoded = input
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder());
const results = await gather(decoded);
expect(results.length).toBe(1);
expect(results[0]).toEqual({
event: "end",
data: null,
});
});
});
-450
View File
@@ -1,450 +0,0 @@
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 } from "msw";
import { useStream } from "../react/stream.js";
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: "agent",
apiKey: "test-api-key",
});
return (
<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>
<div data-testid="loading">
{isLoading ? "Loading..." : "Not loading"}
</div>
{error ? <div data-testid="error">{String(error)}</div> : null}
<button
data-testid="submit"
onClick={() =>
submit({ messages: [{ content: "Hello", type: "human" }] })
}
>
Send
</button>
<button data-testid="stop" onClick={stop}>
Stop
</button>
</div>
);
}
describe("useStream", () => {
beforeEach(() => server.listen());
afterEach(() => {
server.resetHandlers();
server.close();
vi.clearAllMocks();
});
it("renders initial state correctly", () => {
render(<TestChatComponent />);
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
expect(screen.getByTestId("messages")).toBeEmptyDOMElement();
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
it("handles message submission and streaming", async () => {
const user = userEvent.setup();
render(<TestChatComponent />);
// Check loading state
await user.click(screen.getByTestId("submit"));
expect(screen.getByTestId("loading")).toHaveTextContent("Loading...");
// Wait for messages to appear
await waitFor(() => {
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
});
// Check final state
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
});
it("handles stop functionality", async () => {
const user = userEvent.setup();
render(<TestChatComponent />);
// Start streaming and stop immediately
await user.click(screen.getByTestId("submit"));
await user.click(screen.getByTestId("stop"));
// Check loading state is reset
await waitFor(() => {
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();
});
});
});
-101
View File
@@ -1,101 +0,0 @@
type ImageDetail = "auto" | "low" | "high";
type MessageContentImageUrl = {
type: "image_url";
image_url: string | { url: string; detail?: ImageDetail | undefined };
};
type MessageContentText = { type: "text"; text: string };
type MessageContentComplex = MessageContentText | MessageContentImageUrl;
type MessageContent = string | MessageContentComplex[];
/**
* Model-specific additional kwargs, which is passed back to the underlying LLM.
*/
type MessageAdditionalKwargs = Record<string, unknown>;
type BaseMessage = {
additional_kwargs?: MessageAdditionalKwargs | undefined;
content: MessageContent;
id?: string | undefined;
name?: string | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type HumanMessage = BaseMessage & {
type: "human";
example?: boolean | undefined;
};
export type AIMessage = BaseMessage & {
type: "ai";
example?: boolean | undefined;
tool_calls?:
| {
name: string;
args: { [x: string]: any };
id?: string | undefined;
type?: "tool_call" | undefined;
}[]
| undefined;
invalid_tool_calls?:
| {
name?: string | undefined;
args?: string | undefined;
id?: string | undefined;
error?: string | undefined;
type?: "invalid_tool_call" | undefined;
}[]
| undefined;
usage_metadata?:
| {
input_tokens: number;
output_tokens: number;
total_tokens: number;
input_token_details?:
| {
audio?: number | undefined;
cache_read?: number | undefined;
cache_creation?: number | undefined;
}
| undefined;
output_token_details?:
| { audio?: number | undefined; reasoning?: number | undefined }
| undefined;
}
| undefined;
};
export type ToolMessage = BaseMessage & {
type: "tool";
status?: "error" | "success" | undefined;
tool_call_id: string;
/**
* Artifact of the Tool execution which is not meant to be sent to the model.
*
* Should only be specified if it is different from the message content, e.g. if only
* a subset of the full tool output is being passed as message content but the full
* output is needed in other parts of the code.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
artifact?: any;
};
export type SystemMessage = BaseMessage & {
type: "system";
};
export type FunctionMessage = BaseMessage & {
type: "function";
};
export type RemoveMessage = BaseMessage & {
type: "remove";
};
export type Message =
| HumanMessage
| AIMessage
| ToolMessage
| SystemMessage
| FunctionMessage
| RemoveMessage;
-231
View File
@@ -1,231 +0,0 @@
import type { Message } from "./types.messages.js";
/**
* Stream modes
* - "values": Stream only the state values.
* - "messages": Stream complete messages.
* - "messages-tuple": Stream (message chunk, metadata) tuples.
* - "updates": Stream updates to the state.
* - "events": Stream events occurring during execution.
* - "debug": Stream detailed debug information.
* - "custom": Stream custom events.
*/
export type StreamMode =
| "values"
| "messages"
| "updates"
| "events"
| "debug"
| "custom"
| "messages-tuple";
type MessageTupleMetadata = {
tags: string[];
[key: string]: unknown;
};
type AsSubgraph<TEvent extends { id?: string; event: string; data: unknown }> =
{
id?: TEvent["id"];
event: TEvent["event"] | `${TEvent["event"]}|${string}`;
data: TEvent["data"];
};
/**
* Stream event with values after completion of each step.
*/
export type ValuesStreamEvent<StateType> = {
id?: string;
event: "values";
data: StateType;
};
/** @internal */
export type SubgraphValuesStreamEvent<StateType> = AsSubgraph<
ValuesStreamEvent<StateType>
>;
/**
* Stream event with message chunks coming from LLM invocations inside nodes.
*/
export type MessagesTupleStreamEvent = {
event: "messages";
// TODO: add types for message and config, which do not depend on LangChain
// while making sure it's easy to keep them in sync.
data: [message: Message, config: MessageTupleMetadata];
};
/** @internal */
export type SubgraphMessagesTupleStreamEvent =
AsSubgraph<MessagesTupleStreamEvent>;
/**
* Metadata stream event with information about the run and thread
*/
export type MetadataStreamEvent = {
id?: string;
event: "metadata";
data: { run_id: string; thread_id: string };
};
/**
* Stream event with error information.
*/
export type ErrorStreamEvent = {
id?: string;
event: "error";
data: { error: string; message: string };
};
/** @internal */
export type SubgraphErrorStreamEvent = AsSubgraph<ErrorStreamEvent>;
/**
* Stream event with updates to the state after each step.
* The streamed outputs include the name of the node that
* produced the update as well as the update.
*/
export type UpdatesStreamEvent<UpdateType> = {
id?: string;
event: "updates";
data: { [node: string]: UpdateType };
};
/** @internal */
export type SubgraphUpdatesStreamEvent<UpdateType> = AsSubgraph<
UpdatesStreamEvent<UpdateType>
>;
/**
* Streaming custom data from inside the nodes.
*/
export type CustomStreamEvent<T> = { event: "custom"; data: T };
/** @internal */
export type SubgraphCustomStreamEvent<T> = AsSubgraph<CustomStreamEvent<T>>;
type MessagesMetadataStreamEvent = {
id?: string;
event: "messages/metadata";
data: { [messageId: string]: { metadata: unknown } };
};
type MessagesCompleteStreamEvent = {
id?: string;
event: "messages/complete";
data: Message[];
};
type MessagesPartialStreamEvent = {
id?: string;
event: "messages/partial";
data: Message[];
};
/**
* Message stream event specific to LangGraph Server.
* @deprecated Use `streamMode: "messages-tuple"` instead.
*/
export type MessagesStreamEvent =
| MessagesMetadataStreamEvent
| MessagesCompleteStreamEvent
| MessagesPartialStreamEvent;
/** @internal */
export type SubgraphMessagesStreamEvent =
| AsSubgraph<MessagesMetadataStreamEvent>
| AsSubgraph<MessagesCompleteStreamEvent>
| AsSubgraph<MessagesPartialStreamEvent>;
/**
* Stream event with detailed debug information.
*/
export type DebugStreamEvent = { id?: string; event: "debug"; data: unknown };
/** @internal */
export type SubgraphDebugStreamEvent = AsSubgraph<DebugStreamEvent>;
/**
* Stream event with events occurring during execution.
*/
export type EventsStreamEvent = {
id?: string;
event: "events";
data: {
event:
| `on_${"chat_model" | "llm" | "chain" | "tool" | "retriever" | "prompt"}_${"start" | "stream" | "end"}`
| (string & {});
name: string;
tags: string[];
run_id: string;
metadata: Record<string, unknown>;
parent_ids: string[];
data: unknown;
};
};
/** @internal */
export type SubgraphEventsStreamEvent = AsSubgraph<EventsStreamEvent>;
/**
* Stream event with a feedback key to signed URL map. Set `feedbackKeys` in
* the `RunsStreamPayload` to receive this event.
*/
export type FeedbackStreamEvent = {
id?: string;
event: "feedback";
data: { [feedbackKey: string]: string };
};
type GetStreamModeMap<
TStreamMode extends StreamMode | StreamMode[],
TStateType = unknown,
TUpdateType = TStateType,
TCustomType = unknown,
> =
| {
values: ValuesStreamEvent<TStateType>;
updates: UpdatesStreamEvent<TUpdateType>;
custom: CustomStreamEvent<TCustomType>;
debug: DebugStreamEvent;
messages: MessagesStreamEvent;
"messages-tuple": MessagesTupleStreamEvent;
events: EventsStreamEvent;
}[TStreamMode extends StreamMode[] ? TStreamMode[number] : TStreamMode]
| ErrorStreamEvent
| MetadataStreamEvent
| FeedbackStreamEvent;
type GetSubgraphsStreamModeMap<
TStreamMode extends StreamMode | StreamMode[],
TStateType = unknown,
TUpdateType = TStateType,
TCustomType = unknown,
> =
| {
values: SubgraphValuesStreamEvent<TStateType>;
updates: SubgraphUpdatesStreamEvent<TUpdateType>;
custom: SubgraphCustomStreamEvent<TCustomType>;
debug: SubgraphDebugStreamEvent;
messages: SubgraphMessagesStreamEvent;
"messages-tuple": SubgraphMessagesTupleStreamEvent;
events: SubgraphEventsStreamEvent;
}[TStreamMode extends StreamMode[] ? TStreamMode[number] : TStreamMode]
| SubgraphErrorStreamEvent
| MetadataStreamEvent
| FeedbackStreamEvent;
export type TypedAsyncGenerator<
TStreamMode extends StreamMode | StreamMode[] = [],
TSubgraphs extends boolean = false,
TStateType = unknown,
TUpdateType = TStateType,
TCustomType = unknown,
> = AsyncGenerator<
TSubgraphs extends true
? GetSubgraphsStreamModeMap<
TStreamMode,
TStateType,
TUpdateType,
TCustomType
>
: GetStreamModeMap<TStreamMode, TStateType, TUpdateType, TCustomType>
>;
-209
View File
@@ -1,209 +0,0 @@
import { LangChainTracer } from "@langchain/core/tracers/tracer_langchain";
import { Checkpoint, Config, Metadata } from "./schema.js";
import { StreamMode } from "./types.stream.js";
export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
export type OnConflictBehavior = "raise" | "do_nothing";
export type OnCompletionBehavior = "complete" | "continue";
export type DisconnectMode = "cancel" | "continue";
export type StreamEvent =
| "events"
| "metadata"
| "debug"
| "updates"
| "values"
| "messages/partial"
| "messages/metadata"
| "messages/complete"
| "messages"
| (string & {});
export interface Send {
node: string;
input: unknown | null;
}
export interface Command {
/**
* An object to update the thread state with.
*/
update?: Record<string, unknown> | [string, unknown][] | null;
/**
* The value to return from an `interrupt` function call.
*/
resume?: unknown;
/**
* Determine the next node to navigate to. Can be one of the following:
* - Name(s) of the node names to navigate to next.
* - `Send` command(s) to execute node(s) with provided input.
*/
goto?: Send | Send[] | string | string[];
}
export interface RunsInvokePayload {
/**
* Input to the run. Pass `null` to resume from the current state of the thread.
*/
input?: Record<string, unknown> | null;
/**
* Metadata for the run.
*/
metadata?: Metadata;
/**
* Additional configuration for the run.
*/
config?: Config;
/**
* Checkpoint ID for when creating a new run.
*/
checkpointId?: string;
/**
* Checkpoint for when creating a new run.
*/
checkpoint?: Omit<Checkpoint, "thread_id">;
/**
* Whether to checkpoint during the run (or only at the end/interruption).
*/
checkpointDuring?: boolean;
/**
* Interrupt execution before entering these nodes.
*/
interruptBefore?: "*" | string[];
/**
* Interrupt execution after leaving these nodes.
*/
interruptAfter?: "*" | string[];
/**
* Strategy to handle concurrent runs on the same thread. Only relevant if
* there is a pending/inflight run on the same thread. One of:
* - "reject": Reject the new run.
* - "interrupt": Interrupt the current run, keeping steps completed until now,
and start a new one.
* - "rollback": Cancel and delete the existing run, rolling back the thread to
the state before it had started, then start the new run.
* - "enqueue": Queue up the new run to start after the current run finishes.
*/
multitaskStrategy?: MultitaskStrategy;
/**
* Abort controller signal to cancel the run.
*/
signal?: AbortController["signal"];
/**
* Behavior to handle run completion. Only relevant if
* there is a pending/inflight run on the same thread. One of:
* - "complete": Complete the run.
* - "continue": Continue the run.
*/
onCompletion?: OnCompletionBehavior;
/**
* Webhook to call when the run is complete.
*/
webhook?: string;
/**
* Behavior to handle disconnection. Only relevant if
* there is a pending/inflight run on the same thread. One of:
* - "cancel": Cancel the run.
* - "continue": Continue the run.
*/
onDisconnect?: DisconnectMode;
/**
* The number of seconds to wait before starting the run.
* Use to schedule future runs.
*/
afterSeconds?: number;
/**
* Behavior if the specified run doesn't exist. Defaults to "reject".
*/
ifNotExists?: "create" | "reject";
/**
* One or more commands to invoke the graph with.
*/
command?: Command;
/**
* Callback when a run is created.
*/
onRunCreated?: (params: { run_id: string; thread_id?: string }) => void;
/**
* @internal
* For LangSmith tracing purposes only. Not part of the public API.
*/
_langsmithTracer?: LangChainTracer;
}
export interface RunsStreamPayload<
TStreamMode extends StreamMode | StreamMode[] = [],
TSubgraphs extends boolean = false,
> extends RunsInvokePayload {
/**
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
*/
streamMode?: TStreamMode;
/**
* Stream output from subgraphs. By default, streams only the top graph.
*/
streamSubgraphs?: TSubgraphs;
/**
* Whether the stream is considered resumable.
* If true, the stream can be resumed and replayed in its entirety even after disconnection.
*/
streamResumable?: boolean;
/**
* Pass one or more feedbackKeys if you want to request short-lived signed URLs
* for submitting feedback to LangSmith with this key for this run.
*/
feedbackKeys?: string[];
}
export interface RunsCreatePayload extends RunsInvokePayload {
/**
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
*/
streamMode?: StreamMode | Array<StreamMode>;
/**
* Stream output from subgraphs. By default, streams only the top graph.
*/
streamSubgraphs?: boolean;
/**
* Whether the stream is considered resumable.
* If true, the stream can be resumed and replayed in its entirety even after disconnection.
*/
streamResumable?: boolean;
}
export interface CronsCreatePayload extends RunsCreatePayload {
/**
* Schedule for running the Cron Job
*/
schedule: string;
}
export interface RunsWaitPayload extends RunsStreamPayload {
/**
* Raise errors returned by the run. Default is `true`.
*/
raiseError?: boolean;
}
-216
View File
@@ -1,216 +0,0 @@
import pRetry from "p-retry";
import PQueueMod from "p-queue";
import { _getFetchImplementation } from "../singletons/fetch.js";
const STATUS_NO_RETRY = [
400, // Bad Request
401, // Unauthorized
402, // Payment required
403, // Forbidden
404, // Not Found
405, // Method Not Allowed
406, // Not Acceptable
407, // Proxy Authentication Required
408, // Request Timeout
409, // Conflict
422, // Unprocessable Entity
];
type ResponseCallback = (response?: Response) => Promise<boolean>;
export interface AsyncCallerParams {
/**
* The maximum number of concurrent calls that can be made.
* Defaults to `Infinity`, which means no limit.
*/
maxConcurrency?: number;
/**
* The maximum number of retries that can be made for a single call,
* with an exponential backoff between each attempt. Defaults to 6.
*/
maxRetries?: number;
onFailedResponseHook?: ResponseCallback;
/**
* Specify a custom fetch implementation.
*
* By default we expect the `fetch` is available in the global scope.
*/
fetch?: typeof fetch | ((...args: any[]) => any);
}
export interface AsyncCallerCallOptions {
signal?: AbortSignal;
}
/**
* Do not rely on globalThis.Response, rather just
* do duck typing
*/
function isResponse(x: unknown): x is Response {
if (x == null || typeof x !== "object") return false;
return "status" in x && "statusText" in x && "text" in x;
}
/**
* Utility error to properly handle failed requests
*/
class HTTPError extends Error {
status: number;
text: string;
response?: Response;
constructor(status: number, message: string, response?: Response) {
super(`HTTP ${status}: ${message}`);
this.status = status;
this.text = message;
this.response = response;
}
static async fromResponse(
response: Response,
options?: { includeResponse?: boolean },
): Promise<HTTPError> {
try {
return new HTTPError(
response.status,
await response.text(),
options?.includeResponse ? response : undefined,
);
} catch {
return new HTTPError(
response.status,
response.statusText,
options?.includeResponse ? response : undefined,
);
}
}
}
/**
* A class that can be used to make async calls with concurrency and retry logic.
*
* This is useful for making calls to any kind of "expensive" external resource,
* be it because it's rate-limited, subject to network issues, etc.
*
* Concurrent calls are limited by the `maxConcurrency` parameter, which defaults
* to `Infinity`. This means that by default, all calls will be made in parallel.
*
* Retries are limited by the `maxRetries` parameter, which defaults to 5. This
* means that by default, each call will be retried up to 5 times, with an
* exponential backoff between each attempt.
*/
export class AsyncCaller {
protected maxConcurrency: AsyncCallerParams["maxConcurrency"];
protected maxRetries: AsyncCallerParams["maxRetries"];
private queue: (typeof import("p-queue"))["default"]["prototype"];
private onFailedResponseHook?: ResponseCallback;
private customFetch?: typeof fetch;
constructor(params: AsyncCallerParams) {
this.maxConcurrency = params.maxConcurrency ?? Infinity;
this.maxRetries = params.maxRetries ?? 4;
if ("default" in PQueueMod) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.queue = new (PQueueMod.default as any)({
concurrency: this.maxConcurrency,
});
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.queue = new (PQueueMod as any)({ concurrency: this.maxConcurrency });
}
this.onFailedResponseHook = params?.onFailedResponseHook;
this.customFetch = params.fetch;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
call<A extends any[], T extends (...args: A) => Promise<any>>(
callable: T,
...args: Parameters<T>
): Promise<Awaited<ReturnType<T>>> {
const onFailedResponseHook = this.onFailedResponseHook;
return this.queue.add(
() =>
pRetry(
() =>
callable(...(args as Parameters<T>)).catch(async (error) => {
// eslint-disable-next-line no-instanceof/no-instanceof
if (error instanceof Error) {
throw error;
} else if (isResponse(error)) {
throw await HTTPError.fromResponse(error, {
includeResponse: !!onFailedResponseHook,
});
} else {
throw new Error(error);
}
}),
{
async onFailedAttempt(error) {
if (
error.message.startsWith("Cancel") ||
error.message.startsWith("TimeoutError") ||
error.message.startsWith("AbortError")
) {
throw error;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any)?.code === "ECONNABORTED") {
throw error;
}
if (error instanceof HTTPError) {
if (STATUS_NO_RETRY.includes(error.status)) {
throw error;
}
if (onFailedResponseHook && error.response) {
await onFailedResponseHook(error.response);
}
}
},
// If needed we can change some of the defaults here,
// but they're quite sensible.
retries: this.maxRetries,
randomize: true,
},
),
{ throwOnTimeout: true },
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callWithOptions<A extends any[], T extends (...args: A) => Promise<any>>(
options: AsyncCallerCallOptions,
callable: T,
...args: Parameters<T>
): Promise<Awaited<ReturnType<T>>> {
// Note this doesn't cancel the underlying request,
// when available prefer to use the signal option of the underlying call
if (options.signal) {
return Promise.race([
this.call<A, T>(callable, ...args),
new Promise<never>((_, reject) => {
options.signal?.addEventListener("abort", () => {
reject(new Error("AbortError"));
});
}),
]);
}
return this.call<A, T>(callable, ...args);
}
fetch(...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
const fetchFn =
this.customFetch ?? (_getFetchImplementation() as typeof fetch);
return this.call(() =>
fetchFn(...args).then((res) => (res.ok ? res : Promise.reject(res))),
);
}
}
-11
View File
@@ -1,11 +0,0 @@
export function getEnvironmentVariable(name: string): string | undefined {
// Certain setups (Deno, frontend) will throw an error if you try to access environment variables
try {
return typeof process !== "undefined"
? // eslint-disable-next-line no-process-env
process.env?.[name]
: undefined;
} catch (e) {
return undefined;
}
}
-22
View File
@@ -1,22 +0,0 @@
export function mergeSignals(...signals: (AbortSignal | null | undefined)[]) {
const nonZeroSignals = signals.filter(
(signal): signal is AbortSignal => signal != null,
);
if (nonZeroSignals.length === 0) return undefined;
if (nonZeroSignals.length === 1) return nonZeroSignals[0];
const controller = new AbortController();
for (const signal of signals) {
if (signal?.aborted) {
controller.abort(signal.reason);
return controller.signal;
}
signal?.addEventListener("abort", () => controller.abort(signal.reason), {
once: true,
});
}
return controller.signal;
}
-175
View File
@@ -1,175 +0,0 @@
const CR = "\r".charCodeAt(0);
const LF = "\n".charCodeAt(0);
const NULL = "\0".charCodeAt(0);
const COLON = ":".charCodeAt(0);
const SPACE = " ".charCodeAt(0);
const TRAILING_NEWLINE = [CR, LF];
export function BytesLineDecoder() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
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;
}
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
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 (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
return acc;
},
{ lines: [], from: 0 },
);
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 (!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);
}
},
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
interface StreamPart {
id: string | undefined;
event: string;
data: unknown;
}
export function SSEDecoder() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
const decoder = new TextDecoder();
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,
};
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
controller.enqueue(sse);
return;
}
// Ignore comments
if (chunk[0] === COLON) 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);
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,
});
}
},
});
}
function joinArrays(data: ArrayLike<number>[]) {
const totalLength = data.reduce((acc, curr) => acc + curr.length, 0);
let merged = new Uint8Array(totalLength);
let offset = 0;
for (const c of data) {
merged.set(c, offset);
offset += c.length;
}
return merged;
}
function decodeArraysToJson(decoder: TextDecoder, data: ArrayLike<number>[]) {
return JSON.parse(decoder.decode(joinArrays(data)));
}
-115
View File
@@ -1,115 +0,0 @@
// in this case don't quite match.
type IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;
/*
* Support async iterator syntax for ReadableStreams in all environments.
* Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
*/
export class IterableReadableStream<T>
extends ReadableStream<T>
implements IterableReadableStreamInterface<T>
{
public reader: ReadableStreamDefaultReader<T>;
ensureReader() {
if (!this.reader) {
this.reader = this.getReader();
}
}
async next(): Promise<IteratorResult<T>> {
this.ensureReader();
try {
const result = await this.reader.read();
if (result.done) {
this.reader.releaseLock(); // release lock when stream becomes closed
return {
done: true,
value: undefined,
};
} else {
return {
done: false,
value: result.value,
};
}
} catch (e) {
this.reader.releaseLock(); // release lock when stream becomes errored
throw e;
}
}
async return(): Promise<IteratorResult<T>> {
this.ensureReader();
// If wrapped in a Node stream, cancel is already called.
if (this.locked) {
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
this.reader.releaseLock(); // release lock first
await cancelPromise; // now await it
}
return { done: true, value: undefined };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async throw(e: any): Promise<IteratorResult<T>> {
this.ensureReader();
if (this.locked) {
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
this.reader.releaseLock(); // release lock first
await cancelPromise; // now await it
}
throw e;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Not present in Node 18 types, required in latest Node 22
async [Symbol.asyncDispose]() {
await this.return();
}
[Symbol.asyncIterator]() {
return this;
}
static fromReadableStream<T>(stream: ReadableStream<T>) {
// From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream
const reader = stream.getReader();
return new IterableReadableStream<T>({
start(controller) {
return pump();
function pump(): Promise<T | undefined> {
return reader.read().then(({ done, value }) => {
// When no more data needs to be consumed, close the stream
if (done) {
controller.close();
return;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
return pump();
});
}
},
cancel() {
reader.releaseLock();
},
});
}
static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {
return new IterableReadableStream<T>({
async pull(controller) {
const { value, done } = await generator.next();
// When no more data needs to be consumed, close the stream
if (done) {
controller.close();
}
// Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled
controller.enqueue(value);
},
async cancel(reason) {
await generator.return(reason);
},
});
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"declaration": false
},
"exclude": ["node_modules", "dist", "**/tests"]
}
-33
View File
@@ -1,33 +0,0 @@
{
"extends": "@tsconfig/recommended",
"compilerOptions": {
"target": "ES2021",
"lib": ["ES2021", "ES2022.Object", "ES2022.Error", "DOM"],
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"declaration": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"strictPropertyInitialization": false,
"allowJs": true,
"strict": true,
"jsx": "react-jsx",
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage"],
"includeVersion": true,
"typedocOptions": {
"entryPoints": ["src/client.ts"],
"readme": "none",
"out": "docs",
"plugin": ["typedoc-plugin-markdown"],
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": false
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"pageTitleTemplates": {
"index": "{projectName}/auth"
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"pageTitleTemplates": {
"index": "{projectName}/react"
}
}
-11
View File
@@ -1,11 +0,0 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
},
});
File diff suppressed because it is too large Load Diff