diff --git a/docs/docs/cloud/how-tos/use_stream_react.md b/docs/docs/cloud/how-tos/use_stream_react.md
new file mode 100644
index 000000000..89fd8482a
--- /dev/null
+++ b/docs/docs/cloud/how-tos/use_stream_react.md
@@ -0,0 +1,410 @@
+# How to integrate LangGraph into your React application
+
+!!! info "Prerequisites"
+ - [LangGraph Platform](../../concepts/langgraph_platform.md)
+ - [LangGraph Server](../../concepts/langgraph_server.md)
+
+The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
+
+Key features:
+
+- Messages streaming: Handle a stream of message chunks to form a complete message
+- Automatic state management for messages, loading states, and errors
+- Conversation branching: Create alternate conversation paths from any point in the chat history
+- UI-agnostic design - bring your own components and styling
+
+Let's explore how to use `useStream()` in your React application.
+
+The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://github.com/langchain-ai/assistant-ui).
+
+## Example
+
+```tsx
+"use client";
+
+import { useStream } from "@langchain/langgraph-sdk/react";
+import type { Message } from "@langchain/langgraph-sdk";
+
+export default function App() {
+ const thread = useStream<{ messages: Message[] }>({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+ });
+
+ return (
+
+
+ {thread.messages.map((message) => (
+
{message.content as string}
+ ))}
+
+
+
+
+ );
+}
+```
+
+## Customizing Your UI
+
+The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
+
+- Thread state management
+- Loading and error states
+- Message handling and updates
+- Branching support
+
+Here are some examples on how to use these features effectively:
+
+### Loading States
+
+The `isLoading` property tells you when a stream is active, enabling you to:
+
+- Show a loading indicator
+- Disable input fields during processing
+- Display a cancel button
+
+```tsx
+export default function App() {
+ const { isLoading, stop } = useStream<{ messages: Message[] }>({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+ });
+
+ return (
+
+ );
+}
+```
+
+### Thread Management
+
+Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
+
+```tsx
+const [threadId, setThreadId] = useState(null);
+
+const thread = useStream<{ messages: Message[] }>({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+
+ threadId: threadId,
+ onThreadId: setThreadId,
+});
+```
+
+We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
+
+### Messages Handling
+
+To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
+
+When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
+
+```tsx
+import type { Message } from "@langchain/langgraph-sdk";
+import { useStream } from "@langchain/langgraph-sdk/react";
+
+export default function HomePage() {
+ const thread = useStream<{ messages: Message[] }>({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+ });
+
+ return (
+
+ {thread.messages.map((message) => (
+
{message.content as string}
+ ))}
+
+ );
+}
+```
+
+### Branching Support
+
+To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
+
+A branch can be created in following ways:
+
+1. Edit a previous user message.
+2. Request a regeneration of a previous assistant message.
+
+```tsx
+/* eslint-disable @typescript-eslint/no-floating-promises */
+"use client";
+
+import type { Message } from "@langchain/langgraph-sdk";
+import { useStream } from "@langchain/langgraph-sdk/react";
+import {
+ Annotation,
+ MessagesAnnotation,
+ type StateType,
+ type UpdateType,
+} from "@langchain/langgraph/web";
+import { useState } from "react";
+
+const AgentState = Annotation.Root({
+ ...MessagesAnnotation.spec,
+});
+
+function BranchSwitcher({
+ branch,
+ branchOptions,
+ onSelect,
+}: {
+ branch: string | undefined;
+ branchOptions: string[] | undefined;
+ onSelect: (branch: string) => void;
+}) {
+ if (!branchOptions || !branch) return null;
+ const index = branchOptions.indexOf(branch);
+
+ return (
+
+ {
+ const prevBranch = branchOptions[index - 1];
+ if (!prevBranch) return;
+ onSelect(prevBranch);
+ }}
+ >
+ Prev
+
+
+ {index + 1} / {branchOptions.length}
+
+ {
+ const nextBranch = branchOptions[index + 1];
+ if (!nextBranch) return;
+ onSelect(nextBranch);
+ }}
+ >
+ Next
+
+
+ );
+}
+
+function EditMessage({
+ message,
+ onEdit,
+}: {
+ message: Message;
+ onEdit: (message: Message) => void;
+}) {
+ const [editing, setEditing] = useState(false);
+
+ if (!editing) {
+ return (
+ setEditing(true)}>
+ Edit
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default function App() {
+ const thread = useStream<
+ StateType,
+ UpdateType
+ >({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+ });
+
+ return (
+
+
+ {thread.messages.map((message) => {
+ const meta = thread.getMessagesMetadata(message);
+ const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
+
+ return (
+
+
{message.content as string}
+
+ {message.type === "human" && (
+
+ thread.submit(
+ { messages: [message] },
+ { checkpoint: parentCheckpoint }
+ )
+ }
+ />
+ )}
+
+ {message.type === "ai" && (
+
+ thread.submit(undefined, { checkpoint: parentCheckpoint })
+ }
+ >
+ Regenerate
+
+ )}
+
+ thread.setBranch(branch)}
+ />
+
+ );
+ })}
+
+
+
+
+ );
+}
+```
+
+### TypeScript
+
+The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
+
+- State shape
+- Update format
+- Custom events
+
+```tsx
+// Define your types
+type State = {
+ messages: Message[];
+ context?: Record;
+};
+
+type Update = {
+ messages: Message[] | Message;
+ context?: Record;
+};
+
+type CustomEvent = {
+ type: "progress" | "debug";
+ payload: unknown;
+};
+
+// Use them with the hook
+const thread = useStream({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+});
+```
+
+If you're using LangGraph.js, you can reuse your graph's annotation types:
+
+```tsx
+import {
+ Annotation,
+ MessagesAnnotation,
+ type StateType,
+ type UpdateType,
+} from "@langchain/langgraph/web";
+
+const AgentState = Annotation.Root({
+ ...MessagesAnnotation.spec,
+ context: Annotation.Optional(Annotation.Any()),
+});
+
+const thread = useStream<
+ StateType,
+ UpdateType
+>({
+ apiUrl: "http://localhost:2024",
+ assistantId: "agent",
+ messagesKey: "messages",
+});
+```
+
+## Event Handling
+
+The `useStream()` hook provides several callback options to help you respond to different events:
+
+- `onError`: Called when an error occurs.
+- `onFinish`: Called when the stream is finished.
+- `onUpdateEvent`: Called when an update event is received.
+- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
+- `onMetadataEvent`: Called when a metadata event is received.
+
+## Learn More
+
+- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index 22cd938c4..ed1e8b2ab 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -204,6 +204,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
+- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
### Deployment
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 56603d4d6..b90f9128c 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -253,6 +253,7 @@ nav:
- cloud/how-tos/stream_events.md
- cloud/how-tos/stream_debug.md
- cloud/how-tos/stream_multiple.md
+ - cloud/how-tos/use_stream_react.md
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop_1
- cloud/how-tos/human_in_the_loop_breakpoint.md
diff --git a/libs/sdk-js/.gitignore b/libs/sdk-js/.gitignore
index 4d7124e5f..8eaba233a 100644
--- a/libs/sdk-js/.gitignore
+++ b/libs/sdk-js/.gitignore
@@ -13,3 +13,4 @@ react.d.cts
node_modules
dist
.yarn
+docs
\ No newline at end of file
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index 759708e4d..cb34bb373 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.40",
+ "version": "0.0.41",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -10,7 +10,8 @@
"prepublish": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
- "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts"
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
+ "typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json"
},
"main": "index.js",
"license": "MIT",
@@ -33,8 +34,8 @@
"jest": "^29.7.0",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
- "typedoc": "^0.26.1",
- "typedoc-plugin-markdown": "^4.1.0",
+ "typedoc": "^0.27.7",
+ "typedoc-plugin-markdown": "^4.4.2",
"typescript": "^5.4.5",
"react": "^18.3.1"
},
diff --git a/libs/sdk-js/src/react/stream.tsx b/libs/sdk-js/src/react/stream.tsx
index 0f229da40..9919dfbc5 100644
--- a/libs/sdk-js/src/react/stream.tsx
+++ b/libs/sdk-js/src/react/stream.tsx
@@ -128,13 +128,145 @@ interface ValidSequence {
}
export type MessageMetadata> = {
+ /**
+ * The ID of the message used.
+ */
messageId: string;
+
+ /**
+ * The first thread state the message was seen in.
+ */
firstSeenState: ThreadState | undefined;
+ /**
+ * The branch of the message.
+ */
branch: string | undefined;
+
+ /**
+ * The list of branches this message is part of.
+ * This is useful for displaying branching controls.
+ */
branchOptions: string[] | undefined;
};
+function getBranchSequence>(
+ history: ThreadState[],
+) {
+ const childrenMap: Record[]> = {};
+
+ // First pass - collect nodes for each checkpoint
+ history.forEach((state) => {
+ const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
+ childrenMap[checkpointId] ??= [];
+ childrenMap[checkpointId].push(state);
+ });
+
+ // Second pass - create a tree of sequences
+ type Task = { id: string; sequence: Sequence; path: string[] };
+ const rootSequence: Sequence = { type: "sequence", items: [] };
+ const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }];
+
+ const paths: string[][] = [];
+
+ const visited = new Set();
+ while (queue.length > 0) {
+ const task = queue.shift()!;
+ if (visited.has(task.id)) continue;
+ visited.add(task.id);
+
+ const children = childrenMap[task.id];
+ if (children == null || children.length === 0) continue;
+
+ // If we've encountered a fork (2+ children), push the fork
+ // to the sequence and add a new sequence for each child
+ let fork: Fork | undefined;
+ if (children.length > 1) {
+ fork = { type: "fork", items: [] };
+ task.sequence.items.push(fork);
+ }
+
+ for (const value of children) {
+ const id = value.checkpoint.checkpoint_id!;
+
+ let sequence = task.sequence;
+ let path = task.path;
+ if (fork != null) {
+ sequence = { type: "sequence", items: [] };
+ fork.items.unshift(sequence);
+
+ path = path.slice();
+ path.push(id);
+ paths.push(path);
+ }
+
+ sequence.items.push({ type: "node", value, path });
+ queue.push({ id, sequence, path });
+ }
+ }
+
+ return { rootSequence, paths };
+}
+
+const PATH_SEP = ">";
+const ROOT_ID = "$";
+
+// Get flat view
+function getBranchView>(
+ sequence: Sequence,
+ paths: string[][],
+ branch: string,
+) {
+ const path = branch.split(PATH_SEP);
+ const pathMap: Record = {};
+
+ for (const path of paths) {
+ const parent = path.at(-2) ?? ROOT_ID;
+ pathMap[parent] ??= [];
+ pathMap[parent].unshift(path);
+ }
+
+ const history: ThreadState[] = [];
+ const branchByCheckpoint: Record<
+ string,
+ { branch: string | undefined; branchOptions: string[] | undefined }
+ > = {};
+
+ const forkStack = path.slice();
+ const queue: (Node | Fork)[] = [...sequence.items];
+
+ while (queue.length > 0) {
+ const item = queue.shift()!;
+
+ if (item.type === "node") {
+ history.push(item.value);
+ branchByCheckpoint[item.value.checkpoint.checkpoint_id!] = {
+ branch: item.path.join(PATH_SEP),
+ branchOptions: (item.path.length > 0
+ ? pathMap[item.path.at(-2) ?? ROOT_ID] ?? []
+ : []
+ ).map((p) => p.join(PATH_SEP)),
+ };
+ }
+ if (item.type === "fork") {
+ const forkId = forkStack.shift();
+ const index =
+ forkId != null
+ ? item.items.findIndex((value) => {
+ const firstItem = value.items.at(0);
+ if (!firstItem || firstItem.type !== "node") return false;
+ return firstItem.value.checkpoint.checkpoint_id === forkId;
+ })
+ : -1;
+
+ const nextItems = item.items.at(index)?.items ?? [];
+ queue.push(...nextItems);
+ }
+ }
+
+ return { history, branchByCheckpoint };
+}
+
function fetchHistory>(
client: Client,
threadId: string,
@@ -179,29 +311,189 @@ function useThreadHistory>(
};
}
+const useControllableThreadId = (options?: {
+ threadId?: string | null;
+ onThreadId?: (threadId: string) => void;
+}): [string | null, (threadId: string) => void] => {
+ const [localThreadId, _setLocalThreadId] = useState(
+ options?.threadId ?? null,
+ );
+
+ const onThreadIdRef = useRef(options?.onThreadId);
+ onThreadIdRef.current = options?.onThreadId;
+
+ const onThreadId = useCallback((threadId: string) => {
+ _setLocalThreadId(threadId);
+ onThreadIdRef.current?.(threadId);
+ }, []);
+
+ if (typeof options?.threadId === "undefined") {
+ return [localThreadId, onThreadId];
+ }
+
+ return [options.threadId, onThreadId];
+};
+
+interface UseStreamOptions<
+ StateType extends Record = Record,
+ UpdateType extends Record = Partial,
+ CustomType = unknown,
+> {
+ /**
+ * The ID of the assistant to use.
+ */
+ assistantId: string;
+
+ /**
+ * The URL of the API to use.
+ */
+ apiUrl: ClientConfig["apiUrl"];
+
+ /**
+ * The API key to use.
+ */
+ apiKey?: ClientConfig["apiKey"];
+
+ /**
+ * Specify the key within the state that contains messages.
+ * Defaults to "messages".
+ *
+ * @default "messages"
+ */
+ messagesKey?: string;
+
+ /**
+ * Callback that is called when an error occurs.
+ */
+ onError?: (error: unknown) => void;
+
+ /**
+ * Callback that is called when the stream is finished.
+ */
+ onFinish?: (state: ThreadState) => void;
+
+ /**
+ * Callback that is called when an update event is received.
+ */
+ onUpdateEvent?: (data: UpdatesStreamEvent["data"]) => void;
+
+ /**
+ * Callback that is called when a custom event is received.
+ */
+ onCustomEvent?: (data: CustomStreamEvent["data"]) => void;
+
+ /**
+ * Callback that is called when a metadata event is received.
+ */
+ onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void;
+
+ /**
+ * The ID of the thread to fetch history and current values from.
+ */
+ threadId?: string | null;
+
+ /**
+ * Callback that is called when the thread ID is updated (ie when a new thread is created).
+ */
+ onThreadId?: (threadId: string) => void;
+}
+
+interface UseStream<
+ StateType extends Record = Record,
+ UpdateType extends Record = Partial,
+> {
+ /**
+ * The current values of the thread.
+ */
+ values: StateType;
+
+ /**
+ * Last seen error from the thread or during streaming.
+ */
+ error: unknown;
+
+ /**
+ * Whether the stream is currently running.
+ */
+ isLoading: boolean;
+
+ /**
+ * Stops the stream.
+ */
+ stop: () => void;
+
+ /**
+ * Create and stream a run to the thread.
+ */
+ submit: (values: UpdateType, options?: SubmitOptions) => void;
+
+ /**
+ * The current branch of the thread.
+ */
+ branch: string;
+
+ /**
+ * Set the branch of the thread.
+ */
+ setBranch: (branch: string) => void;
+
+ /**
+ * Flattened history of thread states of a thread.
+ */
+ history: ThreadState[];
+
+ /**
+ * Tree of all branches for the thread.
+ * @experimental
+ */
+ experimental_branchTree: Sequence;
+
+ /**
+ * Messages inferred from the thread.
+ * Will automatically update with incoming message chunks.
+ */
+ messages: Message[];
+
+ /**
+ * Get the metadata for a message, such as first thread state the message
+ * was seen in and branch information.
+
+ * @param message - The message to get the metadata for.
+ * @param index - The index of the message in the thread.
+ * @returns The metadata for the message.
+ */
+ getMessagesMetadata: (
+ message: Message,
+ index?: number,
+ ) => MessageMetadata | undefined;
+}
+
+interface SubmitOptions<
+ StateType extends Record = Record,
+> {
+ config?: Config;
+ checkpoint?: Omit | null;
+ command?: Command;
+ interruptBefore?: "*" | string[];
+ interruptAfter?: "*" | string[];
+ metadata?: Metadata;
+ multitaskStrategy?: MultitaskStrategy;
+ onCompletion?: OnCompletionBehavior;
+ onDisconnect?: DisconnectMode;
+ feedbackKeys?: string[];
+ streamMode?: Array;
+ optimisticValues?:
+ | Partial
+ | ((prev: StateType) => Partial);
+}
+
export function useStream<
StateType extends Record = Record,
UpdateType extends Record = Partial,
CustomType = unknown,
->(options: {
- assistantId: string;
-
- apiUrl: ClientConfig["apiUrl"];
- apiKey?: ClientConfig["apiKey"];
-
- withMessages?: string;
-
- onError?: (error: unknown) => void;
- onFinish?: (state: ThreadState) => void;
-
- onUpdateEvent?: (data: UpdatesStreamEvent["data"]) => void;
- onCustomEvent?: (data: CustomStreamEvent["data"]) => void;
- onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void;
-
- // TODO: can we make threadId uncontrollable / controllable?
- threadId?: string | null;
- onThreadId?: (threadId: string) => void;
-}) {
+>(
+ options: UseStreamOptions,
+): UseStream {
type EventStreamEvent =
| ValuesStreamEvent
| UpdatesStreamEvent
@@ -214,15 +506,17 @@ export function useStream<
| ErrorStreamEvent
| FeedbackStreamEvent;
- const { assistantId, threadId, withMessages, onError, onFinish } = options;
+ let { assistantId, messagesKey, onError, onFinish } = options;
+ messagesKey ??= "messages";
+
const client = useMemo(
() => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }),
[options.apiKey, options.apiUrl],
);
+ const [threadId, onThreadId] = useControllableThreadId(options);
- const [branchPath, setBranchPath] = useState([]);
+ const [branch, setBranch] = useState("");
const [isLoading, setIsLoading] = useState(false);
- const [_, setEvents] = useState([]);
const [streamError, setStreamError] = useState(undefined);
const [streamValues, setStreamValues] = useState(null);
@@ -269,118 +563,20 @@ export function useStream<
);
const getMessages = useMemo(() => {
- if (withMessages == null) return undefined;
return (value: StateType) =>
- Array.isArray(value[withMessages])
- ? (value[withMessages] as Message[])
+ Array.isArray(value[messagesKey])
+ ? (value[messagesKey] as Message[])
: [];
- }, [withMessages]);
+ }, [messagesKey]);
- const [sequence, pathMap] = (() => {
- const childrenMap: Record[]> = {};
+ const { rootSequence, paths } = getBranchSequence(history.data);
+ const { history: flatHistory, branchByCheckpoint } = getBranchView(
+ rootSequence,
+ paths,
+ branch,
+ );
- // First pass - collect nodes for each checkpoint
- history.data.forEach((state) => {
- const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
- childrenMap[checkpointId] ??= [];
- childrenMap[checkpointId].push(state);
- });
-
- // Second pass - create a tree of sequences
- type Task = { id: string; sequence: Sequence; path: string[] };
- const rootSequence: Sequence = { type: "sequence", items: [] };
- const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }];
-
- const paths: string[][] = [];
-
- const visited = new Set();
- while (queue.length > 0) {
- const task = queue.shift()!;
- if (visited.has(task.id)) continue;
- visited.add(task.id);
-
- const children = childrenMap[task.id];
- if (children == null || children.length === 0) continue;
-
- // If we've encountered a fork (2+ children), push the fork
- // to the sequence and add a new sequence for each child
- let fork: Fork | undefined;
- if (children.length > 1) {
- fork = { type: "fork", items: [] };
- task.sequence.items.push(fork);
- }
-
- for (const value of children) {
- const id = value.checkpoint.checkpoint_id!;
-
- let sequence = task.sequence;
- let path = task.path;
- if (fork != null) {
- sequence = { type: "sequence", items: [] };
- fork.items.unshift(sequence);
-
- path = path.slice();
- path.push(id);
- paths.push(path);
- }
-
- sequence.items.push({ type: "node", value, path });
- queue.push({ id, sequence, path });
- }
- }
-
- // Third pass, create a map for available forks
- const pathMap: Record = {};
- for (const path of paths) {
- const parent = path.at(-2) ?? "$";
- pathMap[parent] ??= [];
- pathMap[parent].unshift(path);
- }
-
- return [rootSequence as ValidSequence, pathMap];
- })();
-
- const [flatValues, flatPaths] = (() => {
- const result: ThreadState[] = [];
- const flatPaths: Record<
- string,
- { current: string[] | undefined; branches: string[][] | undefined }
- > = {};
-
- const forkStack = branchPath.slice();
- const queue: (Node | Fork)[] = [...sequence.items];
-
- while (queue.length > 0) {
- const item = queue.shift()!;
-
- if (item.type === "node") {
- result.push(item.value);
- flatPaths[item.value.checkpoint.checkpoint_id!] = {
- current: item.path,
- branches:
- item.path.length > 0 ? pathMap[item.path.at(-2) ?? "$"] ?? [] : [],
- };
- }
- if (item.type === "fork") {
- const forkId = forkStack.shift();
- const index =
- forkId != null
- ? item.items.findIndex((value) => {
- const firstItem = value.items.at(0);
- if (!firstItem || firstItem.type !== "node") return false;
- return firstItem.value.checkpoint.checkpoint_id === forkId;
- })
- : -1;
-
- const nextItems = item.items.at(index)?.items ?? [];
- queue.push(...nextItems);
- }
- }
-
- return [result, flatPaths];
- })();
-
- const threadHead: ThreadState | undefined = flatValues.at(-1);
+ const threadHead: ThreadState | undefined = flatHistory.at(-1);
const historyValues = threadHead?.values ?? ({} as StateType);
const historyError = (() => {
const error = threadHead?.tasks?.at(-1)?.error;
@@ -399,8 +595,6 @@ export function useStream<
})();
const messageMetadata = (() => {
- if (getMessages == null) return undefined;
-
const alreadyShown = new Set();
return getMessages(historyValues).map(
(message, idx): MessageMetadata => {
@@ -416,13 +610,13 @@ export function useStream<
| undefined;
let branch = firstSeen
- ? flatPaths[firstSeen.checkpoint.checkpoint_id!]
+ ? branchByCheckpoint[firstSeen.checkpoint.checkpoint_id!]
: undefined;
- if (!branch?.current?.length) branch = undefined;
+ if (!branch?.branch?.length) branch = undefined;
// serialize branches
- const optionsShown = branch?.branches?.flat(2).join(",");
+ const optionsShown = branch?.branchOptions?.flat(2).join(",");
if (optionsShown) {
if (alreadyShown.has(optionsShown)) branch = undefined;
alreadyShown.add(optionsShown);
@@ -431,8 +625,9 @@ export function useStream<
return {
messageId: messageId.toString(),
firstSeenState: firstSeen,
- branch: branch?.current?.join(">"),
- branchOptions: branch?.branches?.map((b) => b.join(">")),
+
+ branch: branch?.branch,
+ branchOptions: branch?.branchOptions,
};
},
);
@@ -445,22 +640,7 @@ export function useStream<
const submit = async (
values: UpdateType | undefined,
- submitOptions?: {
- config?: Config;
- checkpoint?: Omit | null;
- command?: Command;
- interruptBefore?: "*" | string[];
- interruptAfter?: "*" | string[];
- metadata?: Metadata;
- multitaskStrategy?: MultitaskStrategy;
- onCompletion?: OnCompletionBehavior;
- onDisconnect?: DisconnectMode;
- feedbackKeys?: string[];
- streamMode?: Array;
- optimisticValues?:
- | Partial
- | ((prev: StateType) => Partial);
- },
+ submitOptions?: SubmitOptions,
) => {
try {
setIsLoading(true);
@@ -472,7 +652,7 @@ export function useStream<
let usableThreadId = threadId;
if (!usableThreadId) {
const thread = await client.threads.create();
- options?.onThreadId?.(thread.thread_id);
+ onThreadId(thread.thread_id);
usableThreadId = thread.thread_id;
}
@@ -507,9 +687,10 @@ export function useStream<
// Unbranch things
const newPath = submitOptions?.checkpoint?.checkpoint_id
- ? flatPaths[submitOptions?.checkpoint?.checkpoint_id]?.current
+ ? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
: undefined;
- if (newPath != null) setBranchPath(newPath ?? []);
+
+ if (newPath != null) setBranch(newPath ?? "");
// Assumption: we're setting the initial value
// Used for instant feedback
@@ -530,32 +711,17 @@ export function useStream<
let streamError: StreamError | undefined;
for await (const { event, data } of run) {
- setEvents((events) => [...events, { event, data } as EventStreamEvent]);
-
if (event === "error") {
streamError = new StreamError(data);
break;
}
- if (event === "updates") {
- options.onUpdateEvent?.(data);
- }
-
- if (event === "custom") {
- options.onCustomEvent?.(data);
- }
-
- if (event === "metadata") {
- options.onMetadataEvent?.(data);
- }
-
- if (event === "values") {
- setStreamValues(data);
- }
+ if (event === "updates") options.onUpdateEvent?.(data);
+ if (event === "custom") options.onCustomEvent?.(data);
+ if (event === "metadata") options.onMetadataEvent?.(data);
+ if (event === "values") setStreamValues(data);
if (event === "messages") {
- if (!getMessages) continue;
-
const [serialized] = data;
const messageId = messageManagerRef.current.add(serialized);
@@ -577,15 +743,13 @@ export function useStream<
if (!chunk || index == null) return values;
messages[index] = toMessageDict(chunk);
- return { ...values, [withMessages!]: messages };
+ return { ...values, [messagesKey!]: messages };
});
}
}
// TODO: stream created checkpoints to avoid an unnecessary network request
const result = await history.mutate(usableThreadId);
-
- // TODO: write tests verifying that stream values are properly handled lifecycle-wise
setStreamValues(null);
if (streamError != null) throw streamError;
@@ -615,11 +779,6 @@ export function useStream<
const error = isLoading ? streamError : historyError;
const values = streamValues ?? historyValues;
- const setBranch = useCallback(
- (path: string) => setBranchPath(path.split(">")),
- [setBranchPath],
- );
-
return {
get values() {
trackStreamMode("values");
@@ -631,17 +790,15 @@ export function useStream<
stop,
submit,
+
+ branch,
setBranch,
+ history: flatHistory,
+ experimental_branchTree: rootSequence,
+
get messages() {
trackStreamMode("messages-tuple");
-
- if (getMessages == null) {
- throw new Error(
- "No messages key provided. Make sure that `useStream` contains the `messagesKey` property.",
- );
- }
-
return getMessages(values);
},
@@ -650,13 +807,6 @@ export function useStream<
index?: number,
): MessageMetadata | undefined {
trackStreamMode("messages-tuple");
-
- if (getMessages == null) {
- throw new Error(
- "No messages key provided. Make sure that `useStream` contains the `messagesKey` property.",
- );
- }
-
return messageMetadata?.find(
(m) => m.messageId === (message.id ?? index),
);
diff --git a/libs/sdk-js/typedoc.react.json b/libs/sdk-js/typedoc.react.json
new file mode 100644
index 000000000..6359dac1b
--- /dev/null
+++ b/libs/sdk-js/typedoc.react.json
@@ -0,0 +1,5 @@
+{
+ "pageTitleTemplates": {
+ "index": "{projectName}/react"
+ }
+}
diff --git a/libs/sdk-js/yarn.lock b/libs/sdk-js/yarn.lock
index 4d4e4ccf8..635f0c5f3 100644
--- a/libs/sdk-js/yarn.lock
+++ b/libs/sdk-js/yarn.lock
@@ -301,6 +301,15 @@
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
+"@gerrit0/mini-shiki@^1.24.0":
+ version "1.27.2"
+ resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz#cf2a9fcb08a6581c78fc94821f0c854ec4b9f899"
+ integrity sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==
+ dependencies:
+ "@shikijs/engine-oniguruma" "^1.27.2"
+ "@shikijs/types" "^1.27.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -806,10 +815,26 @@
optionalDependencies:
fsevents "~2.3.2"
-"@shikijs/core@1.9.0":
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.9.0.tgz#ff717fef5e0e9882f0848272699fd8f04d6f9a07"
- integrity sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==
+"@shikijs/engine-oniguruma@^1.27.2":
+ version "1.29.2"
+ resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz#d879717ced61d44e78feab16f701f6edd75434f1"
+ integrity sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==
+ dependencies:
+ "@shikijs/types" "1.29.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+
+"@shikijs/types@1.29.2", "@shikijs/types@^1.27.2":
+ version "1.29.2"
+ resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.29.2.tgz#a93fdb410d1af8360c67bf5fc1d1a68d58e21c4f"
+ integrity sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==
+ dependencies:
+ "@shikijs/vscode-textmate" "^10.0.1"
+ "@types/hast" "^3.0.4"
+
+"@shikijs/vscode-textmate@^10.0.1":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz#d06d45b67ac5e9b0088e3f67ebd3f25c6c3d711a"
+ integrity sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==
"@sinclair/typebox@^0.27.8":
version "0.27.8"
@@ -910,6 +935,13 @@
dependencies:
"@types/node" "*"
+"@types/hast@^3.0.4":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
+ integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
+ dependencies:
+ "@types/unist" "*"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@@ -996,6 +1028,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
+"@types/unist@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
+ integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
+
"@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
@@ -3271,7 +3308,7 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
-minimatch@^9.0.3:
+minimatch@^9.0.3, minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
@@ -3857,13 +3894,6 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-shiki@^1.9.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.9.0.tgz#e4d3a044d9c746aefbea47615e83323fdc3dc361"
- integrity sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==
- dependencies:
- "@shikijs/core" "1.9.0"
-
side-channel@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
@@ -4231,21 +4261,21 @@ typedarray.prototype.slice@^1.0.3:
typed-array-buffer "^1.0.2"
typed-array-byte-offset "^1.0.2"
-typedoc-plugin-markdown@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.1.0.tgz#0969e82d9821c956145a4b8a9a70f4e00bde27e8"
- integrity sha512-sUiEJVaa6+MOFShRy14j1OP/VXC5OLyHNecJ2nKeGuBy2M3YiMatSLoIiddFAqVptSuILJTZiJzCBIY6yzAVyg==
+typedoc-plugin-markdown@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.2.tgz#fc31779595aa9bf00e66709f3894e048345bf7ed"
+ integrity sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg==
-typedoc@^0.26.1:
- version "0.26.1"
- resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.1.tgz#fc43108abdea64929a2e636877e250d5dea50957"
- integrity sha512-APsVXqh93jTlpkLuw6+/IORx7n5LN8hzJV8nvMIrYYaIva0VCq0CoDN7Z3hsRThEYVExI/qoFHnAAxrhG+Wd7Q==
+typedoc@^0.27.7:
+ version "0.27.7"
+ resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.7.tgz#09047ffb5c845f45765de26c68b77260867fe967"
+ integrity sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==
dependencies:
+ "@gerrit0/mini-shiki" "^1.24.0"
lunr "^2.3.9"
markdown-it "^14.1.0"
- minimatch "^9.0.4"
- shiki "^1.9.0"
- yaml "^2.4.5"
+ minimatch "^9.0.5"
+ yaml "^2.6.1"
typescript@^5.4.5:
version "5.4.5"
@@ -4468,10 +4498,10 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-yaml@^2.4.5:
- version "2.4.5"
- resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"
- integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==
+yaml@^2.6.1:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"
+ integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
yargs-parser@^20.2.3:
version "20.2.9"