LangChain Tutorial (TypeScript): streaming agent responses for advanced developers
This tutorial shows how to build a TypeScript LangChain agent that streams tokens and tool events back to the client in real time. You need this when a full response is too slow for the UX, or when you want to surface intermediate reasoning, tool calls, and partial output in a chat UI.
What You'll Need
- •Node.js 18+ and npm
- •A TypeScript project with
ts-nodeortsx - •Packages:
- •
langchain - •
@langchain/openai - •
@langchain/core - •
zod - •
dotenv
- •
- •An OpenAI API key in
.env - •Basic familiarity with LangChain agents and tools
Step-by-Step
- •Start with a minimal TypeScript setup and install the dependencies. I’m using ESM-style imports because that matches current LangChain package structure and keeps the code straightforward.
npm init -y
npm install langchain @langchain/openai @langchain/core zod dotenv
npm install -D typescript tsx @types/node
- •Create your environment file and a TypeScript config that supports modern module syntax. This avoids the common “Cannot use import statement outside a module” problem when running LangChain examples.
OPENAI_API_KEY=your_key_here
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
- •Define a real tool and an agent executor that can stream events. The important part is setting
streaming: trueon the model and usingstreamEvents()on the executor, which gives you token-level output plus structured agent/tool events.
import "dotenv/config";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { createOpenAIToolsAgent, AgentExecutor } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const weatherTool = new DynamicStructuredTool({
name: "get_weather",
description: "Get weather for a city",
schema: z.object({ city: z.string() }),
func: async ({ city }) => `The weather in ${city} is 22°C and sunny.`,
});
const llm = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a concise assistant. Use tools when needed."],
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createOpenAIToolsAgent({
llm,
tools: [weatherTool],
prompt,
});
const executor = new AgentExecutor({
agent,
tools: [weatherTool],
});
- •Stream the response event-by-event instead of waiting for the final answer. In production, this is where you push tokens to SSE, WebSockets, or a terminal renderer.
const input = {
input: "What's the weather in Nairobi? Keep it short.",
};
for await (const event of executor.streamEvents(input, { version: "v2" })) {
if (event.event === "on_chat_model_stream") {
const chunk = event.data.chunk.content;
if (typeof chunk === "string" && chunk.length > 0) {
process.stdout.write(chunk);
}
continue;
}
if (event.event === "on_tool_start") {
console.log(`\n[tool:start] ${event.name}`, event.data.input);
continue;
}
if (event.event === "on_tool_end") {
console.log(`\n[tool:end] ${event.name}`, event.data.output);
continue;
}
}
console.log("\nDone.");
- •If you want just token streaming without tool-event noise, use
stream()on the model directly. This is useful for simple chat endpoints where you only care about partial text and not agent internals.
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const stream = await model.stream("Write one sentence about TypeScript.");
for await (const chunk of stream) {
if (typeof chunk.content === "string") {
process.stdout.write(chunk.content);
}
}
process.stdout.write("\n");
Testing It
Run the file with npx tsx src/index.ts and watch for two things: tool events should appear before the final answer, and tokens should print incrementally instead of all at once. If your tool never fires, your prompt is probably not giving the agent enough reason to call it. If nothing streams until the end, confirm you are using streamEvents() or stream() rather than invoke().
A good test prompt is one that forces a tool call, like asking for weather, exchange rates, or policy lookup data. That gives you a clear signal that both the agent loop and streaming path are working.
Next Steps
- •Add SSE or WebSocket transport so browser clients can consume streamed chunks
- •Persist conversation state with message history before streaming starts
- •Add structured logging around
on_tool_start,on_tool_end, and model latency for observability
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit