Using LangChain with Agentuity — Agentuity Documentation

Using LangChain with Agentuity

Build LangChain agents with Agentuity's deployment runtime, persistent storage, and observability

LangChain provides agent primitives, chains, and tool orchestration, but deploying and running those agents requires infrastructure: state management, credential routing, observability. Agentuity handles that layer. Write your agent logic with LangChain, deploy it on Agentuity with built-in storage, logging, and an AI gateway.

ReAct Agent with Tools

Create a LangChain ReAct agent inside an Agentuity handler. LangChain owns the agent loop, Agentuity owns the infrastructure.

Define your tools and agent with LangChain's standard APIs:

typescriptsrc/agent/basic/index.ts
import {
  AIMessage,
  createAgent as createLangChainAgent,
  createMiddleware,
  tool,
  ToolMessage,
} from 'langchain';
import { ChatOpenAI } from '@langchain/openai';
import * as z from 'zod';
 
// LangChain tools use Zod for input validation
const search = tool(
  async ({ query }) => `Results for: ${query}`,
  {
    name: 'search',
    description: 'Search for information',
    schema: z.object({ query: z.string().describe('The search query') }),
  },
);
 
// Middleware catches tool errors so the agent can recover
const handleToolErrors = createMiddleware({
  name: 'HandleToolErrors',
  wrapToolCall: async (request, handler) => {
    try {
      return await handler(request);
    } catch (error) {
      const toolCallId = request.toolCall.id;
      if (!toolCallId) {
        throw error;
      }
 
      return new ToolMessage({
        content: `Tool error: ${String(error)}`,
        tool_call_id: toolCallId,
      });
    }
  },
});
 
const langchainAgent = createLangChainAgent({
  model: new ChatOpenAI({ model: 'gpt-5.4', temperature: 0.1 }),
  tools: [search],
  middleware: [handleToolErrors],
  systemPrompt: 'You are a helpful assistant. Be concise.',
});

Then wrap the LangChain agent with Agentuity's createAgent() for deployment, schemas, and observability:

typescriptsrc/agent/basic/index.ts
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
export default createAgent('basic', {
  description: 'LangChain ReAct agent with tools and error handling',
  schema: {
    input: s.object({ message: s.string() }),
    output: s.object({ response: s.string() }),
  },
  handler: async (ctx, { message }) => {
    ctx.logger.info('Invoking LangChain agent', { message }); 
 
    const result = await langchainAgent.invoke({
      messages: [{ role: 'user', content: message }],
    });
 
    const lastAiMessage = [...result.messages]
      .reverse()
      .find((message): message is AIMessage => message instanceof AIMessage);
 
    return {
      response: typeof lastAiMessage?.content === 'string'
        ? lastAiMessage.content
        : 'No response generated',
    };
  },
});
  • LangChain tools use tool() with Zod schemas, middleware uses createMiddleware() with wrapToolCall hooks
  • langchainAgent.invoke() returns a messages array containing the full reasoning trace
  • Extract the final AI response by finding the last AIMessage in the array

Streaming with Timeline

Use agent.stream() with streamMode: 'values' to iterate over state snapshots as the agent reasons, calls tools, and generates responses.

// ... langchainAgent and tools defined above
import { AIMessage, ToolMessage, createAgent as createLangChainAgent, tool } from 'langchain';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
 
const langchainAgent = createLangChainAgent({
  model: new ChatOpenAI({ model: 'gpt-5.4', temperature: 0.3 }),
  tools: [search, calculate, getTime],
});
 
// Stream the agent execution, capturing each step
const stream = await langchainAgent.stream(
  { messages: [new HumanMessage(message)] },
  { streamMode: 'values' }, 
);
 
for await (const chunk of stream) {
  const lastMessage = chunk.messages.at(-1);
  if (!lastMessage) {
    continue;
  }
 
  if (lastMessage instanceof AIMessage && (lastMessage.tool_calls?.length ?? 0) > 0) {
    // Tool call in progress: agent decided to use a tool
  } else if (lastMessage instanceof ToolMessage) {
    // Tool result received: observation ready for the next reasoning step
  } else if (lastMessage instanceof AIMessage) {
    // Final AI response: no more tool calls
  }
}

Each snapshot includes the full message history. Tool calls appear as AIMessage instances with a non-empty tool_calls array, while the final response has an empty array.

Structured Output

Use responseFormat with a Zod schema when the agent needs to return typed data instead of prose. LangChain validates the result and returns it on structuredResponse.

// ... langchainAgent and tools defined above
import { createAgent as createLangChainAgent } from 'langchain';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
 
const ContactInfoSchema = z.object({
  name: z.string().describe('Full name of the person'),
  email: z.string().describe('Email address'),
  company: z.string().describe('Company name'),
  role: z.string().describe('Job title'),
});
 
const contactAgent = createLangChainAgent({
  model: new ChatOpenAI({ model: 'gpt-5.4' }),
  tools: [search],
  responseFormat: ContactInfoSchema, 
  systemPrompt: 'Find contact details and return only the requested fields.',
});
 
const result = await contactAgent.invoke({
  messages: [{ role: 'user', content: 'Find contact details for Alice at Acme' }],
});
 
const contact = result.structuredResponse; 
// contact.name, contact.email, contact.company are typed strings

The schema belongs to the LangChain agent, while @agentuity/schema still validates the Agentuity request and response payloads.

Full Examples

Explore complete working examples for each pattern:

Next Steps