diff --git a/idea.md b/idea.md new file mode 100644 index 0000000000..c955710a90 --- /dev/null +++ b/idea.md @@ -0,0 +1,772 @@ +# Proposal: `langchain deploy` for TypeScript-native agent deployment + +## Executive summary + +TypeScript developers should be able to deploy LangChain and LangGraph agents with the same feeling they get from Vite, Next.js, or Astro: write a typed config file, add extension packages, run one command, and get a production-ready deployment. + +The proposed primitive is a TypeScript deployment compiler and CLI that lives in the LangChain ecosystem: + +```ts +// langchain.config.ts +import { defineDeployment } from "langchain/deploy"; +import { discord } from "@langchain/deploy-discord"; +import { web } from "@langchain/deploy-web"; + +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts", + research: "./src/agents/research.ts:graph", + }, + extensions: [ + web({ agent: "support" }), + discord({ agent: "support", route: "/discord" }), + ], +}); +``` + +Under the hood, this does not require a new LangSmith runtime. It compiles into the artifacts LangSmith Deployment already supports today: + +- A `langgraph.json` file with `graphs`, `env`, `node_version`, `http.app`, `store`, `checkpointer`, `webhooks`, and other supported fields. +- A generated Hono app that aggregates all extension routes into the single `http.app` entrypoint LangSmith already accepts. +- Optional generated wrapper modules for intuitive agent paths that omit the `:exportName` suffix. +- CLI calls to the existing LangGraph TypeScript CLI for `dev`, `build`, `dockerfile`, `up`, and `deploy`. + +The product idea is not "a new hosting platform." It is "deployment ergonomics for the hosting platform LangSmith already has." + +## What LangSmith Deployment supports today + +Current LangSmith Deployment has the right primitives, but they are exposed at a low level: + +- Applications are described by `langgraph.json`. +- A deployment can contain multiple graphs under the `graphs` key. +- TypeScript deployments use `node_version: 20` and graph paths such as `./src/graph.ts:graph`. +- `.env` files are already supported through the `env` key, and the deploy CLI also reads API keys from `.env`. +- Cloud deployments can be created or updated with `langgraph deploy`. +- Local development works with `npx @langchain/langgraph-cli dev`. +- Docker images can be built with `npx @langchain/langgraph-cli build`. +- Custom TypeScript routes are supported by exporting a Hono app and pointing `http.app` at it. +- Agent Server already exposes durable runs, threads, assistants, streaming, stores, webhooks, MCP, A2A, CORS, route toggles, and deployment logs. + +The missing piece is not capability. The missing piece is a developer-facing composition layer. + +Today, if a TypeScript developer wants multiple custom endpoints or integrations, they must hand-write an `app.ts`, import every integration directly, manually wire the routes, keep `langgraph.json` in sync, remember the Hono conventions, and understand how `http.app` interacts with deployment. That is workable for advanced users, but it is not an ecosystem primitive. + +## The primitive + +Call the primitive **LangChain Deploy**. + +It has three parts: + +1. `defineDeployment()` for typed deployment configuration. +2. `defineExtension()` for reusable extension packages. +3. `langchain deploy` for generating artifacts and delegating to the existing LangGraph CLI. + +This gives TypeScript developers a first-class deploy experience while preserving LangSmith's existing deployment model. + +## Design principles + +- Compile to current infrastructure. The package should emit `langgraph.json`, Hono route code, and optional wrapper files. It should not require Agent Server features that do not exist. +- Keep the escape hatch. Developers can still provide raw `langgraph` config, a custom Hono app, Dockerfile lines, or direct CLI flags. +- Make the common path small. A one-agent deployment should need a config file and one command. +- Make extension authors powerful but bounded. Extensions can add custom routes, UI assets, webhook handlers, CORS settings, environment requirements, and generated files. They should not replace the Agent Server runtime. +- Treat agents as named resources. Extension code should get typed graph or assistant names from the deployment context, then use the existing LangGraph SDK client. +- Prefer generation over magic. Generated artifacts should be inspectable and committed or ignored depending on project preference. + +## Developer experience + +### Create a deployment + +```bash +npm create langchain-agent +cd my-agent +npm run deploy:dev +``` + +The generated project contains: + +```txt +my-agent/ + langchain.config.ts + package.json + .env + src/ + agents/ + support.ts +``` + +The initial config is small: + +```ts +import { defineDeployment } from "langchain/deploy"; + +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts", + }, +}); +``` + +`"./src/agents/support.ts"` is intentionally nicer than `./src/agents/support.ts:graph`. The compiler can generate a wrapper module when a file uses a default export, or resolve a named export when one is provided: + +```ts +export default graph; +``` + +becomes: + +```json +{ + "graphs": { + "support": "./.langchain/generated/agents/support.ts:graph" + } +} +``` + +If the developer wants exact control, they can write: + +```ts +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts:supportGraph", + }, +}); +``` + +### Add extensions + +Extensions feel like Vite plugins: + +```ts +import { defineDeployment } from "langchain/deploy"; +import { discord } from "@langchain/deploy-discord"; +import { web } from "@langchain/deploy-web"; +import { slack } from "@acme/langchain-deploy-slack"; + +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts", + research: "./src/agents/research.ts", + }, + extensions: [ + web({ + agent: "support", + title: "Support agent", + route: "/", + }), + discord({ + agent: "support", + route: "/discord", + }), + slack({ + agent: "research", + route: "/slack/events", + }), + ], +}); +``` + +The developer does not create `app.ts`. The compiler creates one generated Hono app and registers it through `http.app`. + +### Deploy by environment + +```ts +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts", + }, + targets: { + dev: { + name: "support-agent-dev", + type: "dev", + env: ".env", + }, + production: { + name: "support-agent", + type: "prod", + env: ".env.production", + hostUrl: "https://api.host.langchain.com", + }, + }, +}); +``` + +Then: + +```bash +langchain deploy --target dev +langchain deploy --target production +langchain deploy logs --target production --follow +``` + +This maps to current CLI behavior: + +- `name` maps to `--name` or `LANGSMITH_DEPLOYMENT_NAME`. +- `type` maps to `--deployment-type`. +- `hostUrl` maps to `LANGGRAPH_HOST_URL`. +- `env` maps to the generated `langgraph.json` `env` field. +- The generated config path is passed with `-c`. + +## Generated artifacts + +Given: + +```ts +export default defineDeployment({ + agents: { + support: "./src/agents/support.ts", + }, + extensions: [ + web({ agent: "support" }), + discord({ agent: "support", route: "/discord" }), + ], +}); +``` + +The compiler writes: + +```txt +.langchain/ + generated/ + langgraph.dev.json + app.ts + agents/ + support.ts + manifest.json + types.d.ts +``` + +`langgraph.dev.json`: + +```json +{ + "$schema": "https://langgra.ph/schema.json", + "node_version": "20", + "dependencies": ["."], + "graphs": { + "support": "./.langchain/generated/agents/support.ts:graph" + }, + "env": ".env", + "http": { + "app": "./.langchain/generated/app.ts:app" + } +} +``` + +`app.ts`: + +```ts +import { Hono } from "hono"; +import { createDeploymentContext } from "langchain/deploy/runtime"; +import extension0 from "@langchain/deploy-web"; +import extension1 from "@langchain/deploy-discord"; + +export const app = new Hono(); + +const ctx = createDeploymentContext({ + app, + agents: { + support: { + id: "support", + }, + }, +}); + +extension0.setup(ctx, { agent: "support" }); +extension1.setup(ctx, { agent: "support", route: "/discord" }); +``` + +The important trick: LangSmith only sees one Hono app. The developer sees many extensions. + +## Extension API + +An extension is a typed factory that receives options and returns hooks. + +```ts +import { defineExtension } from "langchain/deploy"; +import { Client } from "@langchain/langgraph-sdk"; + +export default function discord(options: DiscordOptions) { + return defineExtension({ + name: "@langchain/deploy-discord", + + config(ctx) { + ctx.requireEnv("DISCORD_PUBLIC_KEY"); + ctx.requireEnv("DISCORD_BOT_TOKEN"); + }, + + setup(ctx) { + ctx.route.post(options.route ?? "/discord", async (c) => { + const event = await c.req.json(); + const client = new Client(ctx.clientOptions(c)); + + const result = await client.runs.wait(null, options.agent, { + input: { + messages: [ + { + role: "user", + content: event.message, + }, + ], + }, + raiseError: true, + }); + + return c.json(result); + }); + }, + }); +} +``` + +The API should feel familiar to Vite users but map to deployment concepts: + +```ts +type DeploymentExtension = { + name: string; + enforce?: "pre" | "post"; + apply?: "dev" | "build" | "deploy" | ((target: DeploymentTarget) => boolean); + + config?: (ctx: ConfigContext) => void | Partial; + configResolved?: (ctx: ResolvedConfigContext) => void; + generate?: (ctx: GenerateContext) => void | Promise; + setup?: (ctx: RuntimeContext) => void; +}; +``` + +### Extension capabilities + +Extensions can use current LangSmith infrastructure in these ways: + +- Add Hono routes to the generated app. +- Add route-local middleware for the routes they own. +- Add static assets or generated UI bundles served by Hono routes. +- Declare required environment variables. +- Add `http.cors` settings. +- Add `webhooks` policy. +- Add `store` and `checkpointer` configuration. +- Add `dockerfile_lines` for OS-level dependencies. +- Emit generated files that are included in the project dependency. +- Expose LangGraph SDK client configuration for invoking deployed agents through the Agent Server API. + +Extensions should not depend on unavailable runtime hooks. For example, TypeScript custom routes are supported today through Hono, but Python-only middleware and lifespan features should not be presented as TypeScript extension capabilities unless Agent Server adds that support. + +## Agent access from extensions + +Extensions should use the existing LangGraph TypeScript SDK instead of a new deployment-specific agent interface. + +```ts +import { Client } from "@langchain/langgraph-sdk"; + +export default defineExtension({ + name: "my-extension", + + setup(ctx) { + // `ctx` is the runtime context passed to this extension by the + // generated deployment app. It is not a global variable. + ctx.route.post("/ask", async (c) => { + const client = new Client(ctx.clientOptions(c)); + const thread = client.threads.stream({ assistantId: ctx.agents.support }); + + await thread.run.start({ + input: { + messages: [{ role: "user", content: "hello" }], + }, + }); + + const output = await thread.output; + await thread.close(); + + return c.json(output); + }); + }, +}); +``` + +Here, `"support"` is the graph ID from the `agents` map. Agent Server already treats graph IDs as default assistant IDs, and the SDK already exposes the concepts extension authors need: assistants, threads, runs, streaming, store access, and configuration. + +The deployment primitive should only provide the missing context required to construct the SDK client inside generated Hono routes: + +```ts +type RuntimeContext = { + route: Hono; + agents: { + support: "support"; + research: "research"; + }; + clientOptions(c?: HonoContext): ConstructorParameters[0]; +}; +``` + +For real-time routes and UI extensions, extension authors can use the SDK's recommended thread-centric streaming API: + +```ts +const client = new Client(ctx.clientOptions(c)); +const thread = client.threads.stream({ assistantId: ctx.agents.support }); + +await thread.run.start({ + input: { + messages: [{ role: "user", content: "hello" }], + }, +}); + +for await (const message of thread.messages) { + // Forward tokens to a browser, Discord interaction, Slack response, etc. +} + +await thread.close(); +``` + +For webhook-style or background integrations, authors can use non-streaming SDK methods such as `client.runs.create(...)`, `client.runs.wait(...)`, and `client.threads.create(...)`. + +Implementation options: + +- For local dev, `ctx.clientOptions(c)` can derive the current server origin from the Hono request and point `@langchain/langgraph-sdk` at the local dev server. +- For deployed routes, `ctx.clientOptions(c)` can point the SDK at the same deployment URL when available, or use relative Agent Server endpoints when the runtime supports it. +- The context should not re-expose SDK methods. It should make `new Client(...)` easy and type-safe. +- Do not use `RemoteGraph` to call the same deployment from inside a graph. Existing docs warn against same-deployment `RemoteGraph` composition because it can cause resource exhaustion. Route handlers should use the Agent Server run APIs. + +This is already possible by hand today. The primitive packages the setup and keeps extension code aligned with the first-party SDK. + +## CLI + +The CLI should be a thin, friendly wrapper around the existing LangGraph CLI. + +```bash +langchain deploy dev +langchain deploy build +langchain deploy dockerfile Dockerfile +langchain deploy up +langchain deploy --target production +langchain deploy logs --target production --follow +langchain deploy doctor +``` + +### `langchain deploy dev` + +Generates artifacts and runs: + +```bash +npx @langchain/langgraph-cli dev -c .langchain/generated/langgraph.dev.json +``` + +### `langchain deploy build` + +Generates artifacts and runs: + +```bash +npx @langchain/langgraph-cli build -c .langchain/generated/langgraph.production.json -t +``` + +### `langchain deploy` + +Generates artifacts and runs: + +```bash +npx @langchain/langgraph-cli deploy -c .langchain/generated/langgraph.production.json --name --deployment-type +``` + +### `langchain deploy doctor` + +Validates the project before the slow deployment path: + +- Confirms every agent path resolves. +- Confirms graph exports are usable. +- Confirms extension names are unique. +- Confirms route collisions. +- Confirms required environment variables exist for the selected target. +- Warns if a custom route shadows an Agent Server route. +- Warns if an extension adds Dockerfile lines that require Docker for local builds. +- Prints the generated `langgraph.json` path. + +This is a major developer-experience win because most deployment failures today are configuration or packaging mistakes. + +## Config shape + +```ts +type LangChainDeploymentConfig = { + agents: Record; + extensions?: ExtensionInstance[]; + targets?: Record; + + env?: true | string | Record; + nodeVersion?: 20; + apiVersion?: string; + + http?: { + cors?: CorsConfig; + configurableHeaders?: HeaderPolicy; + loggingHeaders?: HeaderPolicy; + enableCustomRouteAuth?: boolean; + disableMeta?: boolean; + disableAssistants?: boolean; + disableRuns?: boolean; + disableThreads?: boolean; + disableStore?: boolean; + disableUi?: boolean; + disableMcp?: boolean; + disableA2a?: boolean; + disableWebhooks?: boolean; + mountPrefix?: string; + }; + + store?: StoreConfig; + checkpointer?: CheckpointerConfig; + webhooks?: WebhooksConfig; + dockerfileLines?: string[]; + + raw?: Record; +}; +``` + +The `raw` escape hatch lets advanced users pass through LangGraph configuration fields before the wrapper supports first-class types. + +## Environment model + +By default: + +```ts +defineDeployment({ + agents: { + support: "./src/agents/support.ts", + }, +}); +``` + +compiles with: + +```json +{ + "env": ".env" +} +``` + +if `.env` exists. + +For multiple targets: + +```ts +defineDeployment({ + targets: { + dev: { env: ".env" }, + staging: { env: ".env.staging" }, + production: { env: ".env.production" }, + }, +}); +``` + +Each target gets a generated `langgraph..json`. + +Secrets remain normal LangSmith deployment environment variables. The primitive can read `.env` for local dev and CLI deploys, but it should not invent a separate secrets store. If a team uses the LangSmith UI to manage production secrets, the target can set: + +```ts +production: { + name: "support-agent", + type: "prod", + env: false, +} +``` + +## Extension examples + +Extensions should be described as packaged deployment capabilities, not as thin option bags. Each extension owns a small slice of the generated Hono app and uses the LangGraph SDK to talk to one or more deployed agents. The developer configures intent: which agent, which route, and which integration-specific options. The extension handles the repeated deployment plumbing. + +### Custom web UI + +```ts +web({ + agent: "support", + route: "/", + title: "Support agent", + theme: { + accent: "#1C3D5A", + }, +}); +``` + +What it does: + +- Serves a small chat UI from the same Agent Server deployment. +- Opens a streaming route that calls `client.threads.stream({ assistantId: "support" })`. +- Handles thread creation, message submission, token streaming, interrupt display, and final state rendering. +- Adds any required static assets to the generated deployment output. + +Why this is helpful: + +Without the extension, a developer has to create a frontend bundle, serve it from Hono, define an API route, instantiate the LangGraph SDK client, stream events, and keep that custom app wired into `langgraph.json`. With the extension, a working UI is attached to the deployed agent with one config entry. The route and agent are explicit, so the compiler can catch route conflicts and invalid agent names. + +### Discord + +```ts +discord({ + agent: "support", + route: "/discord/interactions", +}); +``` + +What it does: + +- Adds the endpoint Discord calls for interaction events. +- Declares required environment variables such as the Discord public key and bot token. +- Verifies the incoming request, converts a Discord message into the agent's input format, calls the configured agent with the LangGraph SDK, and formats the response for Discord. +- Optionally maps Discord channel or user IDs to LangGraph thread IDs so conversations stay stateful. + +Why this is helpful: + +Discord integration has a lot of boilerplate that is unrelated to agent logic: request verification, response deadlines, thread mapping, retries, and route setup. The deployment config makes the binding obvious: Discord events at `/discord/interactions` go to the `support` agent. The extension author packages the operational details once, and every agent developer reuses the same deployment behavior. + +### WhatsApp or Twilio + +```ts +twilioWhatsApp({ + agent: "support", + route: "/twilio/whatsapp", +}); +``` + +What it does: + +- Adds the webhook endpoint Twilio calls for inbound WhatsApp messages. +- Declares required Twilio credentials and webhook verification settings. +- Converts inbound messages into LangGraph run input. +- Uses the SDK to continue a thread for that phone number or start a stateless run. +- Returns TwiML or calls Twilio's API to send an async response. + +Why this is helpful: + +The deployment becomes the integration backend. The developer does not need a separate Express app, serverless function, or queue just to connect Twilio to the agent. The extension can standardize phone-number-to-thread mapping while still letting the developer choose which deployed agent handles the conversation. + +### Agent-to-app API + +```ts +api({ + routes: { + "/api/ask": { + agent: "support", + input: z.object({ message: z.string() }), + stream: true, + }, + }, +}); +``` + +What it does: + +- Adds a public or private HTTP endpoint for an application frontend. +- Validates request bodies with the provided schema. +- Converts the validated body into agent input. +- Streams the result or returns a final JSON response using the SDK. +- Can generate a typed client helper for the app that calls `/api/ask`. + +Why this is helpful: + +Many teams do not want to expose raw Agent Server endpoints directly to their product frontend. They want an application-specific API such as `/api/ask`, `/api/triage`, or `/api/search`. This extension gives them that route without hiding LangSmith. It is still just a Hono route over Agent Server runs, but the repetitive validation and streaming glue are generated from a typed declaration. + +### Deploy-time policy + +```ts +policy({ + disableDocsInProduction: true, + requireWebhookHttps: true, + corsOrigins: ["https://app.example.com"], +}); +``` + +What it does: + +- Applies deployment configuration that already exists in `langgraph.json`. +- Sets production CORS rules. +- Disables metadata or docs routes for production targets. +- Restricts webhook destinations to HTTPS or approved domains. +- Fails `langchain deploy doctor` if a target violates the policy. + +Why this is helpful: + +These settings are easy to forget because they are not part of the agent's graph code. A policy extension lets a platform team publish deployment standards once and have every agent project apply them consistently. It compiles to existing `http` and `webhooks` configuration, so it does not require new LangSmith infrastructure. + +## Why this is compelling + +This creates a marketplace-shaped extension surface without creating a new platform surface. + +Vite became powerful because framework authors could package conventions as plugins. Next.js became approachable because routing, build output, and deployment conventions are encoded into the framework. LangSmith already has the runtime for durable agents, but TypeScript developers still need to assemble deployment details manually. + +LangChain Deploy can make the unit of sharing "an agent deployment extension": + +- `@langchain/deploy-web` +- `@langchain/deploy-discord` +- `@langchain/deploy-slack` +- `@langchain/deploy-whatsapp` +- `@langchain/deploy-twilio` +- `@langchain/deploy-auth0` +- `@langchain/deploy-openapi` +- `@langchain/deploy-admin` + +Each package is just TypeScript, Hono routes, generated files, and current LangSmith config. That means the ecosystem can move quickly without waiting for new Agent Server primitives. + +## What should be out of scope + +- A new runtime separate from Agent Server. +- A second deployment backend. +- A custom secrets manager. +- A new graph execution model. +- Same-deployment `RemoteGraph` composition from inside graph execution. +- TypeScript middleware or lifespan claims beyond what Agent Server supports for TypeScript custom routes. +- Automatic production secret upload unless the existing Control Plane API supports it directly. + +## Implementation plan + +### Phase 1: Compiler and CLI + +- Add `defineDeployment()` and config types. +- Load `langchain.config.ts` with a TS runtime or bundler. +- Generate `langgraph.json`. +- Generate wrappers for default-export agents. +- Detect `.env`. +- Delegate `dev`, `build`, `up`, and `deploy` to the LangGraph CLI. +- Add `doctor`. + +### Phase 2: Extension runtime + +- Add `defineExtension()`. +- Generate one Hono `app.ts`. +- Add route registration, env requirements, route collision detection, and target-aware `apply`. +- Add `ctx.clientOptions()` and typed `ctx.agents` so extensions can construct `@langchain/langgraph-sdk` clients directly. +- Ship `@langchain/deploy-web` as the reference extension. + +### Phase 3: Ecosystem + +- Publish extension authoring docs. +- Add templates for Discord, Slack, WhatsApp, and custom UI extensions. +- Add a gallery of extension packages. +- Add generated type declarations for route and agent names. + +### Phase 4: LangSmith UI integration + +- Surface generated manifest metadata in deployment details if supported. +- Show extension names and routes in build logs or deployment metadata. +- Link custom routes from the deployment page. + +This phase should remain optional. The core value does not depend on LangSmith UI changes. + +## Open questions + +- Should the package live at `langchain/deploy`, `@langchain/deploy`, or inside `@langchain/langgraph-cli`? +- Should generated artifacts be committed, ignored, or configurable? +- Should extension route order follow Vite-style `enforce`, array order only, or both? +- Should the default agent export convention prefer `default`, `graph`, or require explicit symbols in strict mode? +- Can Agent Server expose an officially supported in-process helper for custom routes to create runs without HTTP loopback? +- Should the CLI support GitHub-based deployments, or only local build and deploy flows at first? +- Should production `.env` loading be opt-in to avoid accidental secret inclusion in generated artifacts? + +## The pitch + +LangSmith Deployment already has the runtime. LangChain Deploy would provide the product-shaped developer interface. + +The mental model becomes: + +```txt +agents + extensions + targets => LangSmith deployment artifacts +``` + +For a TypeScript developer, that is a much stronger primitive than "write `langgraph.json`, write one custom Hono app, wire every integration by hand, then remember the right CLI flags." + +It makes LangSmith Deployment feel native to the TypeScript ecosystem while staying honest about what the platform can run today. diff --git a/pipeline/core/builder.py b/pipeline/core/builder.py index 3d07a0e871..4e0b420ef3 100644 --- a/pipeline/core/builder.py +++ b/pipeline/core/builder.py @@ -199,6 +199,10 @@ def _add_suggested_edits_link(self, content: str, input_path: Path) -> str: if relative_path.parts == ("index.mdx",): return content + # Snippet files are imported into other pages — never append page footers. + if "snippets" in relative_path.parts: + return content + # Construct the GitHub URLs edit_url = ( f"https://github.com/langchain-ai/docs/edit/main/src/{relative_path}" diff --git a/pipeline/preprocessors/link_map.py b/pipeline/preprocessors/link_map.py index be28766772..5317230ba0 100644 --- a/pipeline/preprocessors/link_map.py +++ b/pipeline/preprocessors/link_map.py @@ -479,7 +479,15 @@ class LinkMap(TypedDict): "ls.logFeedback": "https://reference.langchain.com/javascript/modules/langsmith.vitest.html#logFeedback", "Client.listExamples": "https://reference.langchain.com/javascript/classes/langsmith.client.Client.html#listexamples", "Example": "https://reference.langchain.com/javascript/interfaces/langsmith.Example.html", + "HITLRequest": "https://reference.langchain.com/javascript/langchain/index/HITLRequest", + "MessageMetadata": "https://reference.langchain.com/javascript/langchain-react/MessageMetadata", + "SubagentDiscoverySnapshot": "https://reference.langchain.com/javascript/langchain-react/SubagentDiscoverySnapshot", + "SubgraphDiscoverySnapshot": "https://reference.langchain.com/javascript/langchain-react/SubgraphDiscoverySnapshot", + "SubmissionQueueEntry": "https://reference.langchain.com/javascript/langchain-react/SubmissionQueueEntry", "useStream": "https://reference.langchain.com/javascript/langchain-react/index/useStream", + "injectStream": "https://reference.langchain.com/javascript/langchain-angular/injectStream", + "client.runs.cancel": "https://reference.langchain.com/javascript/langchain-langgraph-sdk/client/RunsClient/cancel", + "ThreadStateJS": "https://reference.langchain.com/javascript/langchain-langgraph-sdk/index/ThreadState", }, }, { @@ -534,10 +542,13 @@ class LinkMap(TypedDict): "wrapGemini": "functions/langsmith.wrappers_gemini.wrapGemini.html", # LangGraph SDK references "Auth": "langchain-langgraph-sdk/auth/Auth", + "client.runs.cancel": "langchain-langgraph-sdk/client/RunsClient/cancel", "client.runs.stream": "classes/_langchain_langgraph-sdk.client.RunsClient.html#stream", "client.runs.wait": "classes/_langchain_langgraph-sdk.client.RunsClient.html#wait", "client.threads.get_history": "classes/_langchain_langgraph-sdk.client.ThreadsClient.html#getHistory", "client.threads.update_state": "classes/_langchain_langgraph-sdk.client.ThreadsClient.html#updateState", + "ThreadState": "langchain-langgraph-sdk/index/ThreadState", + "ThreadStateJS": "langchain-langgraph-sdk/index/ThreadState", # LangGraph checkpoint references "BaseCheckpointSaver": "langchain-langgraph/index/BaseCheckpointSaver", "BaseStore": "langchain-core/stores/BaseStore", @@ -591,6 +602,7 @@ class LinkMap(TypedDict): "createSummarizationMiddleware": "deepagents/middleware/createSummarizationMiddleware", "TodoListMiddleware": "langchain/index/todoListMiddleware", "HumanInTheLoopMiddleware": "langchain/middleware/humanInTheLoopMiddleware", + "HITLRequest": "langchain/index/HITLRequest", "AnthropicPromptCachingMiddleware": "langchain/index/anthropicPromptCachingMiddleware", "SummarizationMiddleware": "langchain/index/summarizationMiddleware", "createMiddleware": "langchain/index/createMiddleware", @@ -672,7 +684,12 @@ class LinkMap(TypedDict): "listRuns": "langsmith/client/Client/listRuns", "Client.listExamples": "classes/langsmith.client.Client.html#listexamples", "Example": "langchain-core/prompts/Example", + "MessageMetadata": "langchain-react/MessageMetadata", + "SubagentDiscoverySnapshot": "langchain-react/SubagentDiscoverySnapshot", + "SubgraphDiscoverySnapshot": "langchain-react/SubgraphDiscoverySnapshot", + "SubmissionQueueEntry": "langchain-react/SubmissionQueueEntry", "useStream": "langchain-react/index/useStream", + "injectStream": "langchain-angular/injectStream", }, }, { diff --git a/src/code-samples/langchain/middleware-dynamic-prompt.ts b/src/code-samples/langchain/middleware-dynamic-prompt.ts index 6a880d6c22..be374e1b81 100644 --- a/src/code-samples/langchain/middleware-dynamic-prompt.ts +++ b/src/code-samples/langchain/middleware-dynamic-prompt.ts @@ -20,7 +20,7 @@ const agent = createAgent({ // :remove-start: import { FakeListChatModel } from "@langchain/core/utils/testing"; -import type { BaseMessage } from "@langchain/core/messages"; +import type { BaseMessage } from "langchain"; function flattenMessageContent(content: unknown): string { if (typeof content === "string") return content; diff --git a/src/oss/deepagents/frontend/overview.mdx b/src/oss/deepagents/frontend/overview.mdx index 88778b8c51..0f493699fe 100644 --- a/src/oss/deepagents/frontend/overview.mdx +++ b/src/oss/deepagents/frontend/overview.mdx @@ -3,11 +3,22 @@ title: Overview description: Build UIs that display real-time subagent streams, task progress, and sandbox for Deep Agents --- -Build frontends that visualize deep agent workflows in real time. These patterns show how to render subagent progress, task planning, streaming content, and IDE-like sandbox experiences from agents created with `createDeepAgent`. +Build frontends that visualize deep agent workflows in real time. These patterns +show how to render subagent progress, task planning, streaming content, and +IDE-like sandbox experiences from agents created with `createDeepAgent`. + +Deep agents are most useful when the UI makes delegation visible. Instead of +showing a single opaque assistant bubble, the LangChain SDKs expose the +coordinator, subagent discovery, custom state, and sandbox-backed artifacts so +users can inspect how a long-running task is being decomposed and completed. + + +These patterns use the v1 frontend SDK packages. If you are using earlier versions, see the migration guides for [React](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-react/docs/v1-migration.md), [Vue](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-vue/docs/v1-migration.md), [Svelte](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-svelte/docs/v1-migration.md), and [Angular](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-angular/docs/v1-migration.md). + ## Architecture -Deep Agents use a coordinator-worker architecture. The main agent plans tasks and delegates to specialized subagents, each running in isolation. On the frontend, `useStream` surfaces both the coordinator's messages and each subagent's streaming state. +Deep Agents use a coordinator-worker architecture. The main agent plans tasks and delegates to specialized subagents, each running in isolation. On the frontend, the v1 stream handle surfaces coordinator messages on the root stream and exposes subagent discovery snapshots for scoped subagent views. ```mermaid %%{ @@ -20,11 +31,15 @@ Deep Agents use a coordinator-worker architecture. The main agent plans tasks an }%% graph LR FRONTEND["useStream()"] + SELECTORS["selector helpers"] BACKEND["createDeepAgent()"] SUB1["Subagent A"] SUB2["Subagent B"] BACKEND --"stream"--> FRONTEND + FRONTEND --"scope by subagent"--> SELECTORS + SELECTORS --> SUB1 + SELECTORS --> SUB2 FRONTEND --"submit"--> BACKEND BACKEND --"delegate"--> SUB1 BACKEND --"delegate"--> SUB2 @@ -34,7 +49,7 @@ graph LR classDef blueHighlight fill:#E5F4FF,stroke:#006DDD,color:#030710; classDef greenHighlight fill:#F6FFDB,stroke:#6E8900,color:#2E3900; classDef purpleHighlight fill:#EBD0F0,stroke:#885270,color:#441E33; - class FRONTEND blueHighlight; + class FRONTEND,SELECTORS blueHighlight; class BACKEND greenHighlight; class SUB1,SUB2 purpleHighlight; ``` @@ -66,7 +81,7 @@ import { createDeepAgent } from "deepagents"; const agent = createDeepAgent({ tools: [getWeather], - system: "You are a helpful assistant", + systemPrompt: "You are a helpful assistant", subagents: [ { name: "researcher", @@ -78,7 +93,7 @@ const agent = createDeepAgent({ ::: -On the frontend, connect with `useStream` the same way as with `createAgent`. Deep agent patterns use additional `useStream` features like `stream.subagents`, `stream.values.todos`, and `filterSubagentMessages` to render subagent-specific UIs. +On the frontend, connect with @[`useStream`] the same way as with `createAgent`. Pass a [type parameter](/oss/langchain/frontend/overview) for type-safe stream state. Deep agent patterns use `stream.subagents`, selector helpers such as `useMessages(stream, subagent)`, and custom state values like `stream.values.todos` to render subagent-specific UIs. ```ts import { useStream } from "@langchain/react"; @@ -91,10 +106,26 @@ function App() { // Deep agent state beyond messages const todos = stream.values?.todos; - const subagents = stream.subagents; + const subagents = [...stream.subagents.values()]; } ``` +## What the SDK exposes + +Deep agent UIs usually need more than the final answer. The frontend SDK gives +you structured projections for the parts of the run users care about: + +| Projection | Use it for | +| --- | --- | +| `stream.messages` | The coordinator conversation and final synthesis. | +| `stream.subagents` | Live discovery of specialist workers, including status and task metadata. | +| `stream.values` | Shared state such as todos, plans, report sections, sandbox metadata, or any custom key your agent writes. | +| Tool-call state | Rendering filesystem, search, browser, or domain tools as cards with progress and results. | +| Interrupts | Pausing delegated work for user approval or missing input without losing the run state. | + +This lets you build interfaces that feel closer to an IDE, task board, or +workflow monitor than a plain chat transcript. + ## Patterns @@ -115,3 +146,7 @@ The [LangChain frontend patterns](/oss/langchain/frontend/overview), including markdown messages, tool calling, and human-in-the-loop, all work with deep agents too. Deep Agents are built on the same LangGraph runtime, so `useStream` provides the same core API. + +For lower-level graph visualizations, see the +[LangGraph frontend patterns](/oss/langgraph/frontend/overview). They show how +to map graph nodes and state keys directly to UI components. diff --git a/src/oss/deepagents/frontend/sandbox.mdx b/src/oss/deepagents/frontend/sandbox.mdx index 71f819d91b..ec26a0bee9 100644 --- a/src/oss/deepagents/frontend/sandbox.mdx +++ b/src/oss/deepagents/frontend/sandbox.mdx @@ -10,15 +10,20 @@ write, and execute code in an isolated environment, then exposes the sandbox filesystem through a custom API server so the frontend can display files in real time as the agent works. +This page covers the **three-panel UI** (file tree, code viewer, and chat) and +the **custom API routes** that expose the sandbox filesystem to it. For sandbox +providers, lifecycle scoping, seeding files, secrets, deployment, and production +`useStream` configuration, see [Going to production](/oss/deepagents/going-to-production). + import { PatternEmbed } from "/snippets/pattern-embed.jsx"; ## Architecture -The sandbox pattern has three layers: +This setup has three parts: -1. **A deep agent with a sandbox backend:** The agent gets filesystem tools +1. **Deep agent with a sandbox backend:** The agent gets filesystem tools (`read_file`, `write_file`, `edit_file`, `execute`) automatically from the sandbox @@ -36,7 +41,7 @@ The sandbox pattern has three layers: ::: -3. **IDE frontend:** A three-panel layout (file tree, code/diff viewer, chat) +3. **Three-panel frontend:** A file tree, code/diff viewer, and chat panel that syncs files in real time as the agent makes changes ```mermaid @@ -55,7 +60,7 @@ graph LR SANDBOX["Sandbox"] UI --"useStream()"--> AGENT - UI --"/api/sandbox/:threadId/*"--> API + UI --"/sandbox/:threadId/*"--> API AGENT --"read/write/execute"--> SANDBOX API --"ls / read"--> SANDBOX @@ -71,19 +76,15 @@ graph LR ## Sandbox lifecycle -Before diving into the code, it's important to understand how sandboxes are -scoped. The scoping strategy determines who shares a sandbox, how long it -lives, and how it's resolved at runtime. - -### Thread-scoped sandbox (recommended) +Choose how long a sandbox lives and who shares it before wiring the frontend. +See [Sandbox lifecycle](/oss/deepagents/going-to-production#lifecycle) for thread-scoped +vs assistant-scoped sandboxes, async [graph factory](/langsmith/graph-rebuild) +setup, TTL behavior, and SDK invocation examples. -Each LangGraph thread gets its own sandbox. The sandbox ID is stored in the -thread's metadata and resolved at runtime via `getConfig()`. -This is the recommended approach for most applications: - -- Conversations are isolated — file changes in one thread don't affect another -- Sandbox state persists across page reloads (same thread = same sandbox) -- Cleanup is straightforward: when a thread is deleted, its sandbox can be too +This guide uses **thread-scoped sandboxes** by default. The frontend and +custom API server both resolve the sandbox from the LangGraph +[thread](/langsmith/use-threads) ID. That keeps conversations isolated and +lets page reloads reconnect to the same environment when you [persist the thread ID](#thread-creation). ```mermaid sequenceDiagram @@ -96,7 +97,7 @@ sequenceDiagram FE->>LG: POST /threads LG-->>FE: threadId - FE->>HTTP: GET /api/sandbox/:threadId/tree + FE->>HTTP: GET /sandbox/:threadId/tree HTTP->>LG: threads.get(threadId) → metadata.sandbox_id alt No sandbox yet HTTP->>SB: LangSmithSandbox.create() @@ -112,188 +113,79 @@ sequenceDiagram LG->>SB: connect to same sandbox ``` -### Agent-scoped sandbox - -All threads under the same assistant share a single sandbox. Useful for -persistent project environments where you want changes to carry across -conversations: - -:::python - -```python -from langgraph.config import get_config - -def get_sandbox_backend_for_assistant(): - config = get_config() - assistant_id = config.get("metadata", {}).get("assistant_id") - return get_or_create_sandbox_for_assistant(assistant_id) -``` - -::: - -:::js - -```ts -import { getConfig } from "@langchain/langgraph"; - -function getSandboxBackendForAssistant() { - const config = getConfig(); - const assistantId = config.metadata?.assistant_id; - return getOrCreateSandboxForAssistant(assistantId); -} -``` - -::: +For [multi-tenant](/oss/deepagents/going-to-production#multi-tenancy) apps, +scope sandboxes by user or assistant in your backend factory instead. For +demos without LangGraph threads, pass a client-generated session ID in the +API URL. The session ID does not persist across browser sessions. -### User-scoped sandbox +## Connect the agent and API server -Each user gets their own sandbox across all threads. Requires custom -authentication and user identification: +Configure the deep agent with a [sandbox backend](/oss/deepagents/sandboxes) +as described in [Execution environment](/oss/deepagents/going-to-production#execution-environment). +The agent gets filesystem tools and an `execute` tool automatically; no extra +tool configuration is needed. -:::python +Building this UI adds one requirement on top of the production setup: a +**custom API server** which runs outside the agent graph, so both the agent +backend and your file-browsing routes must resolve the **same sandbox** for +each thread. Store the sandbox ID on thread metadata and share a single +lookup function between them. -```python -from langgraph.config import get_config - -def get_sandbox_backend_for_user(): - config = get_config() - user_id = config.get("configurable", {}).get("user_id") - return get_or_create_sandbox_for_user(user_id) -``` - -::: +### Resolve the sandbox from thread metadata :::js -```ts -import { getConfig } from "@langchain/langgraph"; - -function getSandboxBackendForUser() { - const config = getConfig(); - const userId = config.configurable?.user_id; - return getOrCreateSandboxForUser(userId); -} -``` - -::: - -### Session-scoped sandbox (client-side) - -For simpler apps without LangGraph threads, the frontend can generate a -session ID and pass it directly. This approach doesn't persist across -browser sessions and is best for demos or prototyping: - -:::python - -```python -import uuid -import urllib.parse -import urllib.request - -session_id = str(uuid.uuid4()) -query = urllib.parse.urlencode({"sessionId": session_id}) -urllib.request.urlopen(f"http://localhost:2024/api/sandbox/tree?{query}") -``` - -::: - -:::js +Define `getOrCreateSandboxForThread` in a shared module. Both the agent graph +factory and the custom API routes import it: ```ts -const sessionId = crypto.randomUUID(); -fetch(`/api/sandbox/tree?sessionId=${sessionId}`); -``` - -::: - -The rest of this guide uses **thread-scoped sandboxes** as the primary example. - -## Setting up the agent - -### Choose a sandbox provider +// src/api/utils.ts +import { Client } from "@langchain/langgraph-sdk"; +import { LangSmithSandbox } from "deepagents"; +import { SandboxClient } from "langsmith/sandbox"; -:::python - -Deep Agents supports multiple [sandbox providers](/oss/integrations/sandboxes). Any provider that implements the `SandboxBackendProtocol` works: - -```python -from deepagents import create_deep_agent -from deepagents.sandbox import LangSmithSandbox # or DaytonaSandbox, etc. - -sandbox = LangSmithSandbox.create() -agent = create_deep_agent(model="google_genai:gemini-3.5-flash", backend=sandbox) -``` - -::: - -:::js - -Deep Agents supports multiple sandbox providers. Any provider that implements the `SandboxBackendProtocol` works: - -```ts -import { createDeepAgent, LangSmithSandbox } from "deepagents"; - -const sandbox = await LangSmithSandbox.create(); - -export const agent = createDeepAgent({ - model: "google_genai:gemini-3.5-flash", - backend: sandbox, - systemPrompt: "You are an expert developer working on a project in /app.", -}); -``` - -::: - -The agent automatically gets filesystem tools (`read_file`, `write_file`, -`edit_file`, `ls`, `glob`, `grep`) and an `execute` tool for running shell -commands. No tool configuration needed. - -### Resolve a sandbox per thread - -Instead of creating a sandbox at module level (which would be shared across -all threads and may expire), resolve the sandbox per-thread at runtime. The sandbox reads `thread_id` from the LangGraph config via `getConfig()`: - -:::js - -```ts -import { createDeepAgent, LangSmithSandbox } from "deepagents"; -import { getConfig } from "@langchain/langgraph"; - -async function getOrCreateSandboxForThread(threadId: string): Promise { - // Check thread metadata for existing sandbox_id +export async function getOrCreateSandboxForThread(threadId: string) { const client = new Client({ apiUrl: "http://localhost:2024" }); const thread = await client.threads.get(threadId); const sandboxId = thread.metadata?.sandbox_id; if (sandboxId) { - // Reconnect to existing sandbox - return new LangSmithSandbox({ - sandbox: await new SandboxClient().getSandbox(sandboxId), - }); + const existing = await new SandboxClient().getSandbox(sandboxId); + if (existing.status === "ready") { + return new LangSmithSandbox({ sandbox: existing }); + } } - // Create new sandbox and store ID in thread metadata const sandbox = await LangSmithSandbox.create({ templateName: "my-template" }); - await seedSandbox(sandbox); + await seedSandbox(sandbox); // See File transfers below await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } }); return sandbox; } +``` -// Create a sandbox that resolves per-thread at runtime -const sandbox = new LangSmithSandbox({ - resolve: async () => { - const config = getConfig(); - const threadId = config.configurable?.thread_id; - if (!threadId) throw new Error("No thread_id — agent must run on a thread"); - return getOrCreateSandboxForThread(threadId); - }, -}); +Wire the agent as an async [graph factory](/langsmith/graph-rebuild) that reads +`thread_id` from the run config and passes the resolved backend to +`createDeepAgent`: -export const agent = createDeepAgent({ - model: "google_genai:gemini-3.5-flash", - backend: sandbox, - systemPrompt: "You are an expert developer working on a project in /app.", -}); +```ts +// src/agents/deep-agent-ide.ts +import { createDeepAgent } from "deepagents"; +import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + +import { getOrCreateSandboxForThread } from "../api/utils.js"; + +export async function agent(config: LangGraphRunnableConfig) { + const threadId = config.configurable?.thread_id; + if (!threadId) throw new Error("No thread_id — agent must run on a thread"); + + const backend = await getOrCreateSandboxForThread(threadId); + + return createDeepAgent({ + model: "google_genai:gemini-3.5-flash", + backend, + systemPrompt: "You are an expert developer working on a project in /app.", + }); +} ``` ::: @@ -307,7 +199,7 @@ from langgraph.config import get_config def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox: - # Look up or create sandbox based on thread_id + # Look up sandbox_id from thread metadata, create if missing, seed files ... @@ -325,38 +217,26 @@ agent = create_deep_agent( ::: -### Seed the sandbox + + Similar to the example in [Going to production](/oss/deepagents/going-to-production#lifecycle), the + agent is an async graph factory invoked on each run. Store the sandbox ID on + thread metadata so custom `http.app` routes can call the same + `getOrCreateSandboxForThread` helper. Going to production uses provider label + lookup instead when the LangGraph SDK is the only entry point. + -Before the agent runs, populate the sandbox with your project files using -`uploadFiles`: +### Seed project files - - For **LangSmith** sandboxes, the container image and resource limits come from a - [sandbox snapshot](/langsmith/sandbox-snapshots). Pass `templateName` when creating -:::python - the sandbox (see `get_or_create_sandbox_for_thread` above). `upload_files` seeds or updates -::: -:::js - the sandbox (see `getOrCreateSandboxForThread` above). `uploadFiles` seeds or updates -::: - project files at runtime on top of that image. - - -```ts -const SEED_FILES: Record = { - "package.json": JSON.stringify({ name: "my-app", version: "1.0.0" }, null, 2), - "src/index.js": 'console.log("Hello");', -}; - -const encoder = new TextEncoder(); -await sandbox.uploadFiles( - Object.entries(SEED_FILES).map(([path, content]) => [`/app/${path}`, encoder.encode(content)]), -); -``` +Before the agent runs, upload starter files with `uploadFiles` / +`upload_files`. See [File transfers](/oss/deepagents/going-to-production#file-transfers) +for seeding patterns, provider examples, and syncing +[memories](/oss/deepagents/memory) or [skills](/oss/deepagents/skills) into +the sandbox. For LangSmith sandboxes, pass `templateName` from a +[sandbox snapshot](/langsmith/sandbox-snapshots) when creating the container. - Run `sandbox.execute("cd /app && npm install")` after uploading `package.json` to install - dependencies before the agent starts. + Run `sandbox.execute("cd /app && npm install")` after uploading + `package.json` so dependencies are ready before the first agent turn. ## Adding the file browsing API @@ -383,11 +263,9 @@ The sandbox API endpoints use the thread ID as a URL path parameter. This ensures the frontend always accesses the correct sandbox for the current :::python conversation, using the same `get_or_create_sandbox_for_thread` function as the -::: -:::js +::::::js conversation, using the same `getOrCreateSandboxForThread` function as the -::: -agent's backend: +:::agent's backend: :::js @@ -398,7 +276,7 @@ import { getOrCreateSandboxForThread } from "./utils.js"; export const app = new Hono(); -app.get("/api/sandbox/:threadId/tree", async (c) => { +app.get("/sandbox/:threadId/tree", async (c) => { const threadId = c.req.param("threadId"); const rootPath = c.req.query("filePath") || "/app"; @@ -424,7 +302,7 @@ app.get("/api/sandbox/:threadId/tree", async (c) => { return c.json({ path: rootPath, entries, sandboxId: sandbox.id }); }); -app.get("/api/sandbox/:threadId/file", async (c) => { +app.get("/sandbox/:threadId/file", async (c) => { const threadId = c.req.param("threadId"); const filePath = c.req.query("filePath"); if (!filePath) return c.json({ error: "filePath is required" }, 400); @@ -450,14 +328,14 @@ from utils import get_or_create_sandbox_for_thread app = FastAPI() -@app.get("/api/sandbox/{thread_id}/tree") +@app.get("/sandbox/{thread_id}/tree") async def list_tree( thread_id: str = Path(...), - path: str = Query("/app"), + filePath: str = Query("/app"), ): sandbox = await get_or_create_sandbox_for_thread(thread_id) result = await sandbox.aexecute( - f"find {path} -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort" + f"find {filePath} -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort" ) entries = [] for line in result.output.strip().split("\n"): @@ -470,16 +348,16 @@ async def list_tree( "path": full_path, "size": int(size_str), }) - return {"path": path, "entries": entries, "sandbox_id": sandbox.id} + return {"path": filePath, "entries": entries, "sandboxId": sandbox.id} -@app.get("/api/sandbox/{thread_id}/file") +@app.get("/sandbox/{thread_id}/file") async def read_file( thread_id: str = Path(...), - path: str = Query(...), + filePath: str = Query(...), ): sandbox = await get_or_create_sandbox_for_thread(thread_id) - results = await sandbox.adownload_files([path]) - return {"path": path, "content": results[0].content.decode()} + results = await sandbox.adownload_files([filePath]) + return {"path": filePath, "content": results[0].content.decode()} ``` ::: @@ -499,7 +377,10 @@ async def read_file( ### Configure `langgraph.json` Register both the agent graph and the API server. The `http.app` field tells -the LangGraph platform to serve your custom routes alongside the default ones: +the LangGraph platform to serve your custom routes alongside the default ones. +See [application structure](/oss/langgraph/application-structure) and +[LangSmith Deployments](/oss/deepagents/going-to-production#langsmith-deployments) +for the full set of `langgraph.json` options. :::js @@ -507,7 +388,7 @@ the LangGraph platform to serve your custom routes alongside the default ones: { "node_version": "22", "graphs": { - "coding_agent": "./src/agents/my-agent.ts:agent" + "deep_agent_ide": "./src/agents/deep-agent-ide.ts:agent" }, "env": ".env", "http": { @@ -523,7 +404,7 @@ the LangGraph platform to serve your custom routes alongside the default ones: ```json { "graphs": { - "coding_agent": "./src/agents/my_agent.py:agent" + "deep_agent_ide": "./src/agents/my_agent.py:agent" }, "env": ".env", "http": { @@ -546,9 +427,18 @@ local development with `langgraph dev`, that's `http://localhost:2024`. ## Building the frontend The frontend has three panels: a file tree sidebar, a code/diff viewer, and a -chat panel. It uses `useStream` for the agent conversation and the custom API +chat panel. It uses @[`useStream`] for the agent conversation and the custom API endpoints for file browsing. +For production deployment, point `apiUrl` at your +[LangSmith Deployment](/langsmith/deployment), enable +`reconnectOnMount` and `fetchStateHistory`, and pass a stable +`thread_id` on each run. See +[Frontend](/oss/deepagents/going-to-production#frontend) in +[Going to production](/oss/deepagents/going-to-production) for those settings +and for [invoking the agent](/oss/deepagents/going-to-production#invoking-the-agent) +with `thread_id` and runtime `context`. + ### Thread creation Create a LangGraph thread when the page loads and persist its ID in @@ -570,7 +460,7 @@ function IDEPreview() { const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "coding_agent", + assistantId: "deep_agent_ide", threadId, onThreadId: updateThreadId, }); @@ -592,7 +482,6 @@ fresh thread (and sandbox): ```tsx function handleNewThread() { - stream.switchThread(null); updateThreadId(null); } ``` @@ -608,7 +497,7 @@ const AGENT_URL = "http://localhost:2024"; async function fetchTree(threadId: string): Promise { const res = await fetch( - `${AGENT_URL}/api/sandbox/${encodeURIComponent(threadId)}/tree?filePath=/app`, + `${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/tree?filePath=/app`, ); const data = await res.json(); return data.entries.filter((e: FileEntry) => !e.path.includes("node_modules")); @@ -616,7 +505,7 @@ async function fetchTree(threadId: string): Promise { async function fetchFile(threadId: string, path: string): Promise { const res = await fetch( - `${AGENT_URL}/api/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`, + `${AGENT_URL}/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`, ); const data = await res.json(); return data.content ?? null; @@ -641,7 +530,7 @@ const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]); export function IDEPreview() { const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "coding_agent", + assistantId: "deep_agent_ide", }); const processedIds = useRef(new Set()); @@ -669,9 +558,9 @@ export function IDEPreview() { processedIds.current.add(id); if (call.name === "write_file" || call.name === "edit_file") { - refreshSingleFile(call.args.path); + refreshSingleFile(call.args.path ?? call.args.file_path); } else if (call.name === "execute") { - refreshAllFiles(); + refreshTreeAndFiles(); } } }, [stream.messages]); @@ -689,7 +578,7 @@ const processedIds = new Set(); const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "coding_agent", + assistantId: "deep_agent_ide", }); watch( @@ -716,16 +605,16 @@ watch( processedIds.add(id); if (call.name === "write_file" || call.name === "edit_file") { - refreshSingleFile(call.args.path); + refreshSingleFile(call.args.path ?? call.args.file_path); } else if (call.name === "execute") { - refreshAllFiles(); + refreshTreeAndFiles(); } } }, { deep: true }, ); -```` +``` ```svelte Svelte ``` @@ -118,26 +144,25 @@ const stream = useStream({ const AGENT_URL = "http://localhost:2024"; - const { messages, getSubagentsByMessage, submit } = useStream({ + const stream = useStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_subagent_cards", - filterSubagentMessages: true, });
- {#each $messages as msg (msg.id)} - + {#each stream.messages as msg (msg.id)} + + {/each} + {#each [...stream.subagents.values()] as subagent (subagent.id)} + {/each}
``` ```ts Angular -import { Component } from "@angular/core"; -import { useStream } from "@langchain/angular"; +import { Component, computed } from "@angular/core"; +import { injectStream } from "@langchain/angular"; const AGENT_URL = "http://localhost:2024"; @@ -145,33 +170,34 @@ const AGENT_URL = "http://localhost:2024"; selector: "app-deep-agent-chat", template: ` @for (msg of stream.messages(); track msg.id) { - + + } + @for (subagent of subagents(); track subagent.id) { + } `, }) export class DeepAgentChatComponent { - stream = useStream({ + stream = injectStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_subagent_cards", - filterSubagentMessages: true, }); + + subagents = computed(() => [...this.stream.subagents().values()]); } ``` -## Submitting with subgraph streaming +## Submitting messages -When submitting a message, enable subgraph streaming and set an appropriate -recursion limit. Deep agent workflows often involve multiple layers of nested -subagraphs, so a higher recursion limit prevents premature termination: +Submit messages through the root stream. Deep agent workflows often involve +multiple layers of nested subgraphs, so set an appropriate recursion limit if +your agent can delegate deeply: ```ts stream.submit( { messages: [{ type: "human", content: text }] }, - { streamSubgraphs: true } + { config: { recursion_limit: 100 } } ); ``` @@ -181,87 +207,50 @@ most multi-expert setups. You can override this via `config.recursion_limit` if needed. -## The SubagentStreamInterface - -Each subagent exposes a `SubagentStreamInterface` with metadata about the -subagent's task, status, and timing: - -```ts -interface SubagentStreamInterface { - id: string; - status: "pending" | "running" | "complete" | "error"; - messages: BaseMessage[]; - result: string | undefined; - toolCall: { - id: string; - name: string; - args: { - description: string; - subagent_type: string; - [key: string]: unknown; - }; - }; - startedAt: number | undefined; - completedAt: number | undefined; -} -``` - -| Property | Description | -| --- | --- | -| `id` | Unique identifier for this subagent instance | -| `status` | Lifecycle state: `pending` → `running` → `complete` or `error` | -| `messages` | The subagent's own message stream, updated in real time | -| `result` | The final output text, available only when `status` is `"complete"` | -| `toolCall` | The tool call that spawned this subagent, including task metadata | -| `toolCall.args.description` | The task description the coordinator assigned to this subagent | -| `toolCall.args.subagent_type` | The type or name of the specialist (e.g., `"researcher"`, `"analyst"`) | -| `startedAt` | Timestamp when the subagent began executing | -| `completedAt` | Timestamp when the subagent finished | +## The SubagentDiscoverySnapshot -## Linking subagents to messages +Each @[SubagentDiscoverySnapshot] is a lightweight discovery record for a +subagent running inside the thread. It tells your UI that a subagent exists, +where it sits in the subagent tree, and what lifecycle state it is in. -The `getSubagentsByMessage` method returns the subagents spawned by a specific -AI message. This lets you render subagent cards directly beneath the -coordinator message that triggered them: - -```ts -const turnSubagents = stream.getSubagentsByMessage(msg.id); -``` - -This returns an array of `SubagentStreamInterface` objects. If the message -didn't spawn any subagents, it returns an empty array. +The snapshot does **not** include the subagent's streamed messages or tool calls. +Instead, pass the snapshot to selector hooks such as +`useMessages(stream, subagent)` or `useToolCalls(stream, subagent)`. These hooks +use the snapshot namespace to subscribe to the subagent's stream primitives only +when the corresponding card or panel is mounted. ## Building the SubagentCard -Each subagent card shows the specialist's name, task description, streaming -content or final result, and timing information: +Each subagent card shows the specialist's name, status, streaming content, and +tool calls. Use selector hooks to subscribe to the subagent namespace: ```tsx -import { AIMessage } from "@langchain/core/messages"; +import { useState } from "react"; +import { AIMessage } from "langchain"; +import { + useMessages, + useToolCalls, + type AnyStream, + type SubagentDiscoverySnapshot, +} from "@langchain/react"; function SubagentCard({ + stream, subagent, }: { - subagent: SubagentStreamInterface; + stream: AnyStream; + subagent: SubagentDiscoverySnapshot; }) { const [expanded, setExpanded] = useState(true); + const messages = useMessages(stream, subagent); + const toolCalls = useToolCalls(stream, subagent); - const title = - subagent.toolCall?.args?.subagent_type ?? `Agent ${subagent.id}`; - const description = subagent.toolCall?.args?.description ?? ""; - - const lastAIMessage = subagent.messages + const lastAIMessage = messages .filter(AIMessage.isInstance) .at(-1); const displayContent = - subagent.status === "complete" - ? subagent.result - : typeof lastAIMessage?.content === "string" - ? lastAIMessage.content - : ""; - - const elapsed = getElapsedTime(subagent.startedAt, subagent.completedAt); + lastAIMessage?.text ?? subagent.output ?? ""; return (
@@ -272,14 +261,13 @@ function SubagentCard({
-

{title}

-

{description}

+

{subagent.name}

+

+ {toolCalls.length} tool call{toolCalls.length === 1 ? "" : "s"} +

- {elapsed && ( - {elapsed} - )}
@@ -297,51 +285,6 @@ function SubagentCard({
); } - -function getElapsedTime( - startedAt: number | undefined, - completedAt: number | undefined -): string | null { - if (!startedAt) return null; - const end = completedAt ?? Date.now(); - const seconds = Math.round((end - startedAt) / 1000); - if (seconds < 60) return `${seconds}s`; - return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; -} -``` - -## Status icons and badges - -Consistent visual indicators help users parse subagent status at a glance: - -```tsx -function StatusIcon({ status }: { status: SubagentStreamInterface["status"] }) { - switch (status) { - case "pending": - return ; - case "running": - return ; - case "complete": - return ; - case "error": - return ; - } -} - -function StatusBadge({ status }: { status: SubagentStreamInterface["status"] }) { - const styles = { - pending: "bg-gray-100 text-gray-600", - running: "bg-blue-100 text-blue-700", - complete: "bg-green-100 text-green-700", - error: "bg-red-100 text-red-700", - }; - - return ( - - {status} - - ); -} ``` ## Progress tracking @@ -352,7 +295,7 @@ Show a progress bar and counter so users know how many subagents have finished: function SubagentProgress({ subagents, }: { - subagents: SubagentStreamInterface[]; + subagents: SubagentDiscoverySnapshot[]; }) { const completed = subagents.filter((s) => s.status === "complete").length; const total = subagents.length; @@ -379,126 +322,57 @@ function SubagentProgress({ ## Rendering messages with subagent cards -The key layout pattern is to render each coordinator message, and if that message -spawned subagents, render their cards immediately below it: +The key layout pattern is to render coordinator messages from the root stream +and attach subagent cards to the AI message whose tool call spawned them: ```tsx -function MessageWithSubagents({ - message, - subagents, -}: { - message: BaseMessage; - subagents: SubagentStreamInterface[]; -}) { - if (message.type === "human") { - return ; - } +function DeepAgentLayout({ stream }: { stream: AnyStream }) { + const subagents = [...stream.subagents.values()]; + const subagentsByCallId = new Map(subagents.map((s) => [s.id, s])); return (
- {message.content && ( -
- {message.content} -
- )} - - {subagents.length > 0 && ( -
- - {subagents.map((subagent) => ( - - ))} -
- )} -
- ); -} -``` - -## Synthesis indicator - -After all subagents complete, the coordinator takes time to synthesize their -results into a final response. Show a clear indicator during this phase: - -```tsx -function SynthesisIndicator({ - subagents, - isLoading, -}: { - subagents: SubagentStreamInterface[]; - isLoading: boolean; -}) { - const allComplete = - subagents.length > 0 && - subagents.every((s) => s.status === "complete" || s.status === "error"); - - if (!allComplete || !isLoading) return null; - - return ( -
- - Synthesizing results from {subagents.length} subagent - {subagents.length !== 1 ? "s" : ""}... + {stream.messages.map((message) => { + const turnSubagents = AIMessage.isInstance(message) + ? (message.tool_calls ?? []) + .map((tc) => subagentsByCallId.get(tc.id ?? "")) + .filter((s): s is SubagentDiscoverySnapshot => !!s) + : []; + + return ( +
+ + {turnSubagents.length > 0 && ( +
+ + {turnSubagents.map((subagent) => ( + + ))} +
+ )} +
+ ); + })}
); } ``` - -The synthesis phase can take several seconds for complex multi-expert -workflows. A clear "Synthesizing results..." indicator prevents users from -thinking the agent has stalled. - - -## Debug unfiltered output - -During development, you can temporarily set `filterSubagentMessages: false` to -see the raw, interleaved output from all subagents in the main message stream. -This is useful for verifying that subagent tokens are flowing correctly, but -should not be used in production UIs. - -## Use cases - -Deep agent subagent cards are the right choice when your agent workflow -involves: - -- **Deep research** where a coordinator dispatches researchers to investigate - different facets of a question, then synthesizes their findings -- **Multi-expert analysis** such as domain specialists (legal, financial, technical) - each contribute their perspective -- **Complex task decomposition** where a planner breaks a large task into subtasks - and assigns each to a specialist worker -- **Code review pipelines** where separate agents handle security review, style - checking, performance analysis, and documentation review - -## Accessing the full subagents map - -Beyond per-message lookup, you can access all subagents at once through -`stream.subagents`: - -```ts -const allSubagents = [...stream.subagents.values()]; -const running = allSubagents.filter((s) => s.status === "running"); -const completed = allSubagents.filter((s) => s.status === "complete"); -const errors = allSubagents.filter((s) => s.status === "error"); -``` - -This is useful for building global progress indicators or dashboards that -summarize all subagent activity regardless of which coordinator message spawned -them. +You can combine inline cards with a global subagent view: index subagents by +the coordinator tool call that spawned them for transcript cards, and use +`stream.subagents` for a persistent sidebar that summarizes all active workers. +That gives users both local context and a bird's-eye view of the whole run. ## Best practices -- **Always set `filterSubagentMessages: true`**. Unfiltered streams produce an - unreadable interleaving of coordinator and subagent tokens. -- **Show task descriptions**. The `toolCall.args.description` field tells users - exactly what each subagent was asked to do. Always display this prominently. +- **Mount selectors only where needed**. Scoped messages and tool calls stream + when a card calls `useMessages(stream, subagent)` or `useToolCalls(stream, subagent)`. +- **Show specialist names**. `subagent.name` tells users which worker is active. - **Use collapsible cards**. In workflows with 5+ subagents, auto-collapse completed cards so users can focus on active work. -- **Display timing data**. Showing how long each subagent took helps users - understand performance characteristics and identify bottlenecks. -- **Set an appropriate recursion limit**. Deep agent workflows with nested - subgraphs need higher limits than the default 25. Start with 100. +- **Override recursion only when needed**. Deep Agents sets a high default + recursion limit; pass `config.recursion_limit` only for unusually deep custom + workflows. - **Handle errors per subagent**. One subagent failing shouldn't crash the entire UI. Show the error in that subagent's card while others continue running. diff --git a/src/oss/deepagents/frontend/todo-list.mdx b/src/oss/deepagents/frontend/todo-list.mdx index af0792affe..72213b2f93 100644 --- a/src/oss/deepagents/frontend/todo-list.mdx +++ b/src/oss/deepagents/frontend/todo-list.mdx @@ -12,16 +12,17 @@ the agent works through its plan. It's a progress dashboard built on the same not just message bubbles. import { PatternEmbed } from "/snippets/pattern-embed.jsx" +import UseStreamTypeInference from '/snippets/oss/use-stream-type-inference.mdx'; ## How it works -In a LangGraph agent, state isn't limited to messages. You can define **custom -state keys** that hold arbitrary data. In this case, a `todos` array. As the -agent executes its plan, it updates each todo's status from `"pending"` to -`"in_progress"` to `"completed"`. The `useStream` hook exposes these custom -state values via `stream.values`, and your UI renders them reactively. +Deep agents include a built-in **`todos` state** that tracks task progress as +the agent works through its plan. As the agent executes, it updates each +todo's status from `"pending"` to `"in_progress"` to `"completed"`. The +@[`useStream`] hook exposes this state via `stream.values.todos`, and your UI +renders it reactively. The flow looks like this: @@ -32,41 +33,12 @@ The flow looks like this: 4. `stream.values.todos` updates in real time as the agent progresses 5. Your UI re-renders the todo list with current statuses -## Setting up useStream +## Setting up `useStream` -No special configuration is needed. Point `useStream` at your agent and +No special configuration is needed. Point @[`useStream`] at your agent and read the `todos` from `stream.values`. -:::python - -Define a TypeScript interface matching your agent's state schema and pass it as a type parameter to `useStream` for type-safe access to state values, including custom state keys like `todos`. In the examples below, replace `typeof myAgent` with your interface name: - -```ts -import type { BaseMessage } from "@langchain/core/messages"; - -interface TodoItem { - title: string; - status: "pending" | "in_progress" | "completed"; - description?: string; -} - -interface AgentState { - messages: BaseMessage[]; - todos: TodoItem[]; -} -``` - -::: - -:::js - -Import your agent and pass `typeof myAgent` as a type parameter to `useStream` for type-safe access to state values: - -```ts -import type { myAgent } from "./agent"; -``` - -::: + ```tsx React @@ -126,17 +98,17 @@ const todos = computed(() => stream.values.value?.todos ?? []); const AGENT_URL = "http://localhost:2024"; - const { messages, values, submit } = useStream({ + const stream = useStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_todo_list", }); - $: todos = $values?.todos ?? []; + const todos = $derived(stream.values?.todos ?? []);
- {#each $messages as msg (msg.id)} + {#each stream.messages as msg (msg.id)} {/each}
@@ -144,7 +116,7 @@ const todos = computed(() => stream.values.value?.todos ?? []); ```ts Angular import { Component, computed } from "@angular/core"; -import { useStream } from "@langchain/angular"; +import { injectStream } from "@langchain/angular"; const AGENT_URL = "http://localhost:2024"; @@ -160,7 +132,7 @@ const AGENT_URL = "http://localhost:2024"; `, }) export class TodoAgentComponent { - stream = useStream({ + stream = injectStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_todo_list", }); @@ -170,25 +142,6 @@ export class TodoAgentComponent { ```
-## The Todo interface - -Each todo in the array has a simple structure: - -```ts -interface Todo { - status: "pending" | "in_progress" | "completed"; - content: string; -} -``` - -| Property | Description | -| --- | --- | -| `status` | The current state of this task. Options: `pending` (not started), `in_progress` (agent is working on it), `completed` (done) | -| `content` | A human-readable description of what the task involves | - -The agent populates this array when it creates its plan, then updates individual -items as it executes each step. - ## Building the TodoList component The todo list renders each item with a status icon, color coding, and visual @@ -356,72 +309,6 @@ Show the todo list only when `todos.length > 0`. Before the agent creates its plan, there's nothing to display. Showing an empty component wastes space. -## Custom state beyond todos - -This pattern demonstrates a powerful principle: `stream.values` can expose -**any custom state** your agent defines, not just messages. The `todos` array is -just one example. You could use the same approach for: - -- **Progress metrics**: `stream.values.progress` with numeric completion data -- **Generated artifacts**: `stream.values.document` with a structured document - the agent is building -- **Decision logs**: `stream.values.decisions` tracking every choice the agent - made -- **Resource lists**: `stream.values.sources` with links and references the - agent found - -```ts -// Any custom state key your agent defines is accessible -const document = stream.values?.document; -const sources = stream.values?.sources ?? []; -const confidence = stream.values?.confidence_score; -``` - - -Custom state keys are defined in your LangGraph graph's state schema. The -`useStream` hook automatically includes them in `stream.values` without any additional -client-side configuration. - - -## Animating transitions - -Todo status transitions happen in real time, and smooth animations make these -changes feel polished rather than jarring: - -```tsx -function TodoItem({ todo }: { todo: Todo }) { - return ( -
  • - - {getStatusIcon(todo.status)} - - - {todo.content} - -
  • - ); -} -``` - -The `transition-all duration-300` classes ensure that color changes, -strikethrough, and opacity shifts all animate smoothly. - ## Use cases The todo list pattern fits any scenario where an agent executes a structured diff --git a/src/oss/deepagents/going-to-production.mdx b/src/oss/deepagents/going-to-production.mdx index 237af9b2c1..c7391b953a 100644 --- a/src/oss/deepagents/going-to-production.mdx +++ b/src/oss/deepagents/going-to-production.mdx @@ -1039,7 +1039,7 @@ For the complete list of available middleware, see [prebuilt middleware](/oss/la ## Frontend -Deep Agents use [`useStream`](/oss/langchain/frontend/overview) to connect your UI to the agent backend. `useStream` is a frontend hook (available for React, Vue, Svelte, and Angular) that streams messages, subagent progress, and custom state from your agent in real time. +Deep Agents use [`useStream`](/oss/langchain/frontend/overview) to connect your UI to the agent backend. @[`useStream`] is a frontend hook (available for React, Vue, Svelte, and Angular) that streams messages, subagent progress, and custom state from your agent in real time. Locally, `useStream` points at `http://localhost:2024`. In production, point it at your [LangSmith Deployment](/langsmith/deployment) and configure reconnection so users don't lose progress if their connection drops. diff --git a/src/oss/deepagents/streaming.mdx b/src/oss/deepagents/streaming.mdx index 6fabcadbb0..1dca3faa1d 100644 --- a/src/oss/deepagents/streaming.mdx +++ b/src/oss/deepagents/streaming.mdx @@ -936,5 +936,5 @@ See the [LangGraph streaming docs](/oss/langgraph/streaming#stream-output-format ## Related - [Subagents](/oss/deepagents/subagents)—Configure and use subagents with Deep Agents -- [Frontend streaming](/oss/deepagents/streaming/frontend)—Build React UIs with `useStream` for Deep Agents +- [Frontend streaming](/oss/deepagents/streaming/frontend)—Build React UIs with @[`useStream`] for Deep Agents - [LangChain Event Streaming](/oss/langchain/event-streaming)—General streaming concepts with LangChain agents diff --git a/src/oss/langchain/frontend/branching-chat.mdx b/src/oss/langchain/frontend/branching-chat.mdx index 1de775715f..ae4c0c832b 100644 --- a/src/oss/langchain/frontend/branching-chat.mdx +++ b/src/oss/langchain/frontend/branching-chat.mdx @@ -1,66 +1,43 @@ --- title: Branching chat -description: Edit messages, regenerate responses, and navigate conversation branches +description: Edit messages and regenerate responses by forking from checkpoints --- Conversations with AI agents are rarely linear. You may want to rephrase a -question, regenerate a response you didn't like, or explore a completely -different conversational path without losing your previous work. Branching chat -brings version-control semantics to your chat UI. Every edit creates a new -branch, and you can freely navigate between them. +question, regenerate a response you didn't like, or explore a different +conversational path without losing the checkpoint history. Branching chat uses +LangGraph checkpoints as fork points: every edit or regeneration submits a new +run from the selected message's parent checkpoint. import { PatternEmbed } from "/snippets/pattern-embed.jsx" import RequiresLanggraphServer from '/snippets/oss/requires-langgraph-server.mdx'; +import UseStreamTypeInference from '/snippets/oss/use-stream-type-inference.mdx'; ## What is branching chat? -Branching chat treats a conversation as a tree rather than a list. Each message -is a node, and editing a message or regenerating a response creates a **fork** -from that point. The original path is preserved as a sibling branch, so users -can switch back and forth between different conversation trajectories. +Branching chat treats a conversation as a checkpointed timeline rather than a +flat list. Each message has metadata that points to the checkpoint before that +message was created. Editing a message or regenerating a response submits a new +run from that checkpoint. Key capabilities: -- **Edit any user message:** rewrite a previous prompt and re-run the agent - from that point -- **Regenerate any AI response:** ask the agent to produce a different answer - for the same input -- **Navigate branches:** switch between different versions of the conversation - using per-message branch controls +- **Edit any user message:** rewrite a previous prompt and re-run the agent from that point +- **Regenerate any AI response:** ask the agent to produce a different answer for the same input +- **Inspect history:** use the LangGraph client to load checkpoints when you need a branch timeline -## Set up useStream with history +## Set up stream metadata -To enable branching, pass `fetchStateHistory: true` so that `useStream` -retrieves checkpoint metadata needed for branch operations. +Use the root stream for messages, then read per-message checkpoint metadata in +the component that renders each message. The metadata includes the parent +checkpoint ID to fork from. -:::python - -Define a TypeScript interface matching your agent's state schema and pass it as a type parameter to `useStream` for type-safe access to state values. In the examples below, replace `typeof myAgent` with your interface name: - -```ts -import type { BaseMessage } from "@langchain/core/messages"; - -interface AgentState { - messages: BaseMessage[]; -} -``` - -::: - -:::js - -Import your agent and pass `typeof myAgent` as a type parameter to `useStream` for type-safe access to state values: - -```ts -import type { myAgent } from "./agent"; -``` - -::: + ```tsx React @@ -71,25 +48,14 @@ const AGENT_URL = "http://localhost:2024"; export function Chat() { const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "branching_chat", - fetchStateHistory: true, + assistantId: "simple_agent", }); return (
    - {stream.messages.map((msg) => { - const metadata = stream.getMessagesMetadata(msg); - return ( - handleEdit(stream, msg, metadata, text)} - onRegenerate={() => handleRegenerate(stream, metadata)} - onBranchSwitch={(id) => stream.setBranch(id)} - /> - ); - })} + {stream.messages.map((msg) => ( + + ))}
    ); } @@ -103,38 +69,17 @@ const AGENT_URL = "http://localhost:2024"; const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "branching_chat", - fetchStateHistory: true, + assistantId: "simple_agent", }); - -function handleEdit(msg: any, metadata: any, text: string) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - stream.submit( - { messages: [{ ...msg, content: text }] }, - { checkpoint } - ); -} - -function handleRegenerate(metadata: any) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - stream.submit(undefined, { checkpoint }); -} @@ -146,39 +91,17 @@ function handleRegenerate(metadata: any) { const AGENT_URL = "http://localhost:2024"; - const { messages, getMessagesMetadata, setBranch, submit } = useStream({ + const stream = useStream({ apiUrl: AGENT_URL, - assistantId: "branching_chat", - fetchStateHistory: true, + assistantId: "simple_agent", }); - - function handleEdit(msg: any, metadata: any, text: string) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - submit( - { messages: [{ ...msg, content: text }] }, - { checkpoint } - ); - } - - function handleRegenerate(metadata: any) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - submit(undefined, { checkpoint }); - }
    - {#each $messages as msg (msg.id)} - {@const metadata = getMessagesMetadata(msg)} + {#each stream.messages as msg (msg.id)} handleEdit(msg, metadata, text)} - onRegenerate={() => handleRegenerate(metadata)} - onBranchSwitch={(id) => setBranch(id)} + {stream} /> {/each}
    @@ -186,7 +109,7 @@ function handleRegenerate(metadata: any) { ```ts Angular import { Component } from "@angular/core"; -import { useStream } from "@langchain/angular"; +import { injectStream } from "@langchain/angular"; const AGENT_URL = "http://localhost:2024"; @@ -196,122 +119,137 @@ const AGENT_URL = "http://localhost:2024"; @for (msg of stream.messages(); track msg.id) { } `, }) export class ChatComponent { - stream = useStream({ + stream = injectStream({ apiUrl: AGENT_URL, - assistantId: "branching_chat", - fetchStateHistory: true, + assistantId: "simple_agent", }); - - handleEdit(msg: any, metadata: any, text: string) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - this.stream.submit( - { messages: [{ ...msg, content: text }] }, - { checkpoint } - ); - } - - handleRegenerate(metadata: any) { - const checkpoint = metadata.firstSeenState?.parent_checkpoint; - if (!checkpoint) return; - - this.stream.submit(undefined, { checkpoint }); - } } ```
    ## Understand message metadata -The `getMessagesMetadata(msg)` function returns branch information for each -message: +The `useMessageMetadata(stream, messageId)` helper returns @[MessageMetadata] +for one message. Use it in the component that renders each message so the +metadata stays scoped to that message ID: -```ts -interface MessageMetadata { - branch: string; - branchOptions: string[]; - firstSeenState: { - parent_checkpoint: Checkpoint | null; - }; +```tsx +import type { BaseMessage } from "langchain"; +import { useState } from "react"; +import { useMessageMetadata, useStream } from "@langchain/react"; + +function Chat() { + const stream = useStream({ + apiUrl: AGENT_URL, + assistantId: "simple_agent", + }); + + return stream.messages.map((message) => ( + + )); } -``` -| Property | Description | -| --- | --- | -| `branch` | The branch ID of this specific message version | -| `branchOptions` | Array of all branch IDs available for this message position | -| `firstSeenState.parent_checkpoint` | The checkpoint just before this message. Use it as the fork point for edits and regenerations | +function MessageWithForkControls({ + stream, + message, +}: { + stream: ReturnType; + message: BaseMessage; +}) { + const metadata = useMessageMetadata(stream, message.id); + const checkpointId = metadata?.parentCheckpointId; + const [editedText, setEditedText] = useState(message.text); + + return ( +
    { + event.preventDefault(); + if (!checkpointId) return; + + stream.submit( + { messages: [{ type: "human", content: editedText }] }, + { forkFrom: { checkpointId } } + ); + }} + > +