Context engineering in agents

no

Original Documentation

Documentation Index#

Fetch the complete documentation index at: https://docs.langchain.com/llms.txt Use this file to discover all available pages before exploring further.

Overview#

The hard part of building agents (or any LLM application) is making them reliable enough. While they may work for a prototype, they often fail in real-world use cases.

Why do agents fail?#

When agents fail, it’s usually because the LLM call inside the agent took the wrong action / didn’t do what we expected. LLMs fail for one of two reasons:

  1. The underlying LLM is not capable enough
  2. The “right” context was not passed to the LLM

More often than not - it’s actually the second reason that causes agents to not be reliable.

Context engineering is providing the right information and tools in the right format so the LLM can accomplish a task. This is the number one job of AI Engineers. This lack of “right” context is the number one blocker for more reliable agents, and LangChain’s agent abstractions are uniquely designed to facilitate context engineering.

New to context engineering? Start with the conceptual overview to understand the different types of context and when to use them.

The agent loop#

A typical agent loop consists of two main steps:

  1. Model call - calls the LLM with a prompt and available tools, returns either a response or a request to execute tools
  2. Tool execution - executes the tools that the LLM requested, returns tool results
Core agent loop diagram

This loop continues until the LLM decides to finish.

What you can control#

To build reliable agents, you need to control what happens at each step of the agent loop, as well as what happens between steps.

Context TypeWhat You ControlTransient or Persistent
Model ContextWhat goes into model calls (instructions, message history, tools, response format)Transient
Tool ContextWhat tools can access and produce (reads/writes to state, store, runtime context)Persistent
Life-cycle ContextWhat happens between model and tool calls (summarization, guardrails, logging, etc.)Persistent

<span class=“card-start” data-card-raw=“title=“Transient context” icon=“bolt” iconType=“duotone”"> What the LLM sees for a single call. You can modify messages, tools, or prompts without changing what’s saved in state.

<span class=“card-start” data-card-raw=“title=“Persistent context” icon=“database” iconType=“duotone”"> What gets saved in state across turns. Life-cycle hooks and tool writes modify this permanently.

Data sources#

Throughout this process, your agent accesses (reads / writes) different sources of data:

Data SourceAlso Known AsScopeExamples
Runtime ContextStatic configurationConversation-scopedUser ID, API keys, database connections, permissions, environment settings
StateShort-term memoryConversation-scopedCurrent messages, uploaded files, authentication status, tool results
StoreLong-term memoryCross-conversationUser preferences, extracted insights, memories, historical data

How it works#

LangChain middleware is the mechanism under the hood that makes context engineering practical for developers using LangChain.

Middleware allows you to hook into any step in the agent lifecycle and:

  • Update context
  • Jump to a different step in the agent lifecycle

Throughout this guide, you’ll see frequent use of the middleware API as a means to the context engineering end.

Model context#

Control what goes into each model call - instructions, available tools, which model to use, and output format. These decisions directly impact reliability and cost.

Base instructions from the developer to the LLM.

The full list of messages (conversation history) sent to the LLM.

Utilities the agent has access to to take actions.

The actual model (including configuration) to be called.

Schema specification for the model’s final response.

All of these types of model context can draw from state (short-term memory), store (long-term memory), or runtime context (static configuration).

System Prompt#

The system prompt sets the LLM’s behavior and capabilities. Different users, contexts, or conversation stages need different instructions. Successful agents draw on memories, preferences, and configuration to provide the right instructions for the current state of the conversation.

Access message count or conversation context from state:

    import { createAgent } from "langchain";

    const agent = createAgent({
      model: "gpt-4.1",
      tools: [...],
      middleware: [
        dynamicSystemPromptMiddleware((state) => {
          // Read from State: check conversation length
          const messageCount = state.messages.length;

          let base = "You are a helpful assistant.";

          if (messageCount > 10) {
            base += "\nThis is a long conversation - be extra concise.";
          }

          return base;
        }),
      ],
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Access user preferences from long-term memory:

```typescript
    import * as z from "zod";
    import { createAgent, dynamicSystemPromptMiddleware } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    type Context = z.infer<typeof contextSchema>;

    const agent = createAgent({
      model: "gpt-4.1",
      tools: [...],
      contextSchema,
      middleware: [
        dynamicSystemPromptMiddleware<Context>(async (state, runtime) => {
          const userId = runtime.context.userId;

          // Read from Store: get user preferences
          const store = runtime.store;
          const userPrefs = await store.get(["preferences"], userId);

          let base = "You are a helpful assistant.";

          if (userPrefs) {
            const style = userPrefs.value?.communicationStyle || "balanced";
            base += `\nUser prefers ${style} responses.`;
          }

          return base;
        }),
      ],
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Access user ID or configuration from Runtime Context:

```typescript
    import * as z from "zod";
    import { createAgent, dynamicSystemPromptMiddleware } from "langchain";

    const contextSchema = z.object({
      userRole: z.string(),
      deploymentEnv: z.string(),
    });

    type Context = z.infer<typeof contextSchema>;

    const agent = createAgent({
      model: "gpt-4.1",
      tools: [...],
      contextSchema,
      middleware: [
        dynamicSystemPromptMiddleware<Context>((state, runtime) => {
          // Read from Runtime Context: user role and environment
          const userRole = runtime.context.userRole;
          const env = runtime.context.deploymentEnv;

          let base = "You are a helpful assistant.";

          if (userRole === "admin") {
            base += "\nYou have admin access. You can perform all operations.";
          } else if (userRole === "viewer") {
            base += "\nYou have read-only access. Guide users to read operations only.";
          }

          if (env === "production") {
            base += "\nBe extra careful with any data modifications.";
          }

          return base;
        }),
      ],
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

### Messages

Messages make up the prompt that is sent to the LLM.
It's critical to manage the content of messages to ensure that the LLM has the right information to respond well.

<span class="tab-group-start"></span>
  <span class="tab-start" data-tab-title="State"></span>
Inject uploaded file context from State when relevant to current query:

```typescript
    import { createMiddleware } from "langchain";

    const injectFileContext = createMiddleware({
      name: "InjectFileContext",
      wrapModelCall: (request, handler) => {
        // request.state is a shortcut for request.state.messages
        const uploadedFiles = request.state.uploadedFiles || [];  // [!code highlight]

        if (uploadedFiles.length > 0) {
          // Build context about available files
          const fileDescriptions = uploadedFiles.map(file =>
            `- ${file.name} (${file.type}): ${file.summary}`
          );

          const fileContext = `Files you have access to in this conversation:
    ${fileDescriptions.join("\n")}

    Reference these files when answering questions.`;

          // Inject file context before recent messages
          const messages = [  // [!code highlight]
            ...request.messages,  // Rest of conversation
            { role: "user", content: fileContext }
          ];
          request = request.override({ messages });  // [!code highlight]
        }

        return handler(request);
      },
    });

    const agent = createAgent({
      model: "gpt-4.1",
      tools: [...],
      middleware: [injectFileContext],
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Inject user's email writing style from Store to guide drafting:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    const injectWritingStyle = createMiddleware({
      name: "InjectWritingStyle",
      contextSchema,
      wrapModelCall: async (request, handler) => {
        const userId = request.runtime.context.userId;  // [!code highlight]

        // Read from Store: get user's writing style examples
        const store = request.runtime.store;  // [!code highlight]
        const writingStyle = await store.get(["writing_style"], userId);  // [!code highlight]

        if (writingStyle) {
          const style = writingStyle.value;
          // Build style guide from stored examples
          const styleContext = `Your writing style:
    - Tone: ${style.tone || 'professional'}
    - Typical greeting: "${style.greeting || 'Hi'}"
    - Typical sign-off: "${style.signOff || 'Best'}"
    - Example email you've written:
    ${style.exampleEmail || ''}`;

          // Append at end - models pay more attention to final messages
          const messages = [
            ...request.messages,
            { role: "user", content: styleContext }
          ];
          request = request.override({ messages });  // [!code highlight]
        }

        return handler(request);
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Inject compliance rules from Runtime Context based on user's jurisdiction:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userJurisdiction: z.string(),
      industry: z.string(),
      complianceFrameworks: z.array(z.string()),
    });

    type Context = z.infer<typeof contextSchema>;

    const injectComplianceRules = createMiddleware<Context>({
      name: "InjectComplianceRules",
      contextSchema,
      wrapModelCall: (request, handler) => {
        // Read from Runtime Context: get compliance requirements
        const { userJurisdiction, industry, complianceFrameworks } = request.runtime.context;  // [!code highlight]

        // Build compliance constraints
        const rules = [];
        if (complianceFrameworks.includes("GDPR")) {
          rules.push("- Must obtain explicit consent before processing personal data");
          rules.push("- Users have right to data deletion");
        }
        if (complianceFrameworks.includes("HIPAA")) {
          rules.push("- Cannot share patient health information without authorization");
          rules.push("- Must use secure, encrypted communication");
        }
        if (industry === "finance") {
          rules.push("- Cannot provide financial advice without proper disclaimers");
        }

        if (rules.length > 0) {
          const complianceContext = `Compliance requirements for ${userJurisdiction}:
    ${rules.join("\n")}`;

          // Append at end - models pay more attention to final messages
          const messages = [
            ...request.messages,
            { role: "user", content: complianceContext }
          ];
          request = request.override({ messages });  // [!code highlight]
        }

        return handler(request);
      },
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

<span class="callout-start" data-callout-type="note"></span>
  **Transient vs Persistent Message Updates:**

  The examples above use `wrap_model_call` to make **transient** updates - modifying what messages are sent to the model for a single call without changing what's saved in state.

  For **persistent** updates that modify state (like the summarization example in [Life-cycle Context](#summarization)), use life-cycle hooks like `before_model` or `after_model` to permanently update the conversation history. See the [middleware documentation](/oss/javascript/langchain/middleware) for more details.
<span class="callout-end"></span>

### Tools

Tools let the model interact with databases, APIs, and external systems. How you define and select tools directly impacts whether the model can complete tasks effectively.

#### Defining tools

Each tool needs a clear name, description, argument names, and argument descriptions. These aren't just metadata—they guide the model's reasoning about when and how to use the tool.

```typescript


const searchOrders = tool(
  async ({ userId, status, limit }) => {
    // Implementation here
  },
  {
    name: "search_orders",
    description: `Search for user orders by status.

    Use this when the user asks about order history or wants to check
    order status. Always filter by the provided status.`,
    schema: z.object({
      userId: z.string().describe("Unique identifier for the user"),
      status: z.enum(["pending", "shipped", "delivered"]).describe("Order status to filter by"),
      limit: z.number().default(10).describe("Maximum number of results to return"),
    }),
  }
);

Selecting tools#

Not every tool is appropriate for every situation. Too many tools may overwhelm the model (overload context) and increase errors; too few limit capabilities. Dynamic tool selection adapts the available toolset based on authentication state, user permissions, feature flags, or conversation stage.

Enable advanced tools only after certain conversation milestones:

    import { createMiddleware } from "langchain";

    const stateBasedTools = createMiddleware({
      name: "StateBasedTools",
      wrapModelCall: (request, handler) => {
        // Read from State: check authentication and conversation length
        const state = request.state;  // [!code highlight]
        const isAuthenticated = state.authenticated || false;  // [!code highlight]
        const messageCount = state.messages.length;

        let filteredTools = request.tools;

        // Only enable sensitive tools after authentication
        if (!isAuthenticated) {
          filteredTools = request.tools.filter(t => t.name.startsWith("public_"));  // [!code highlight]
        } else if (messageCount < 5) {
          filteredTools = request.tools.filter(t => t.name !== "advanced_search");  // [!code highlight]
        }

        return handler({ ...request, tools: filteredTools });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Filter tools based on user preferences or feature flags in Store:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    const storeBasedTools = createMiddleware({
      name: "StoreBasedTools",
      contextSchema,
      wrapModelCall: async (request, handler) => {
        const userId = request.runtime.context.userId;  // [!code highlight]

        // Read from Store: get user's enabled features
        const store = request.runtime.store;  // [!code highlight]
        const featureFlags = await store.get(["features"], userId);  // [!code highlight]

        let filteredTools = request.tools;

        if (featureFlags) {
          const enabledFeatures = featureFlags.value?.enabledTools || [];
          filteredTools = request.tools.filter(t => enabledFeatures.includes(t.name));  // [!code highlight]
        }

        return handler({ ...request, tools: filteredTools });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Filter tools based on user permissions from Runtime Context:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userRole: z.string(),
    });

    const contextBasedTools = createMiddleware({
      name: "ContextBasedTools",
      contextSchema,
      wrapModelCall: (request, handler) => {
        // Read from Runtime Context: get user role
        const userRole = request.runtime.context.userRole;  // [!code highlight]

        let filteredTools = request.tools;

        if (userRole === "admin") {
          // Admins get all tools
        } else if (userRole === "editor") {
          filteredTools = request.tools.filter(t => t.name !== "delete_data");  // [!code highlight]
        } else {
          filteredTools = request.tools.filter(t => t.name.startsWith("read_"));  // [!code highlight]
        }

        return handler({ ...request, tools: filteredTools });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

See [Dynamic tools](/oss/javascript/langchain/agents#dynamic-tools) for both filtering pre-registered tools and registering tools at runtime (e.g., from MCP servers).

### Model

Different models have different strengths, costs, and context windows. Select the right model for the task at hand, which
might change during an agent run.

<span class="tab-group-start"></span>
  <span class="tab-start" data-tab-title="State"></span>
Use different models based on conversation length from State:

```typescript
    import { createMiddleware, initChatModel } from "langchain";

    // Initialize models once outside the middleware
    const largeModel = initChatModel("claude-sonnet-4-6");
    const standardModel = initChatModel("gpt-4.1");
    const efficientModel = initChatModel("gpt-4.1-mini");

    const stateBasedModel = createMiddleware({
      name: "StateBasedModel",
      wrapModelCall: (request, handler) => {
        // request.messages is a shortcut for request.state.messages
        const messageCount = request.messages.length;  // [!code highlight]
        let model;

        if (messageCount > 20) {
          model = largeModel;
        } else if (messageCount > 10) {
          model = standardModel;
        } else {
          model = efficientModel;
        }

        return handler({ ...request, model });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Use user's preferred model from Store:

```typescript
    import * as z from "zod";
    import { createMiddleware, initChatModel } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    // Initialize available models once
    const MODEL_MAP = {
      "gpt-4.1": initChatModel("gpt-4.1"),
      "gpt-4.1-mini": initChatModel("gpt-4.1-mini"),
      "claude-sonnet": initChatModel("claude-sonnet-4-6"),
    };

    const storeBasedModel = createMiddleware({
      name: "StoreBasedModel",
      contextSchema,
      wrapModelCall: async (request, handler) => {
        const userId = request.runtime.context.userId;  // [!code highlight]

        // Read from Store: get user's preferred model
        const store = request.runtime.store;  // [!code highlight]
        const userPrefs = await store.get(["preferences"], userId);  // [!code highlight]

        let model = request.model;

        if (userPrefs) {
          const preferredModel = userPrefs.value?.preferredModel;
          if (preferredModel && MODEL_MAP[preferredModel]) {
            model = MODEL_MAP[preferredModel];  // [!code highlight]
          }
        }

        return handler({ ...request, model });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Select model based on cost limits or environment from Runtime Context:

```typescript
    import * as z from "zod";
    import { createMiddleware, initChatModel } from "langchain";

    const contextSchema = z.object({
      costTier: z.string(),
      environment: z.string(),
    });

    // Initialize models once outside the middleware
    const premiumModel = initChatModel("claude-sonnet-4-6");
    const standardModel = initChatModel("gpt-4.1");
    const budgetModel = initChatModel("gpt-4.1-mini");

    const contextBasedModel = createMiddleware({
      name: "ContextBasedModel",
      contextSchema,
      wrapModelCall: (request, handler) => {
        // Read from Runtime Context: cost tier and environment
        const costTier = request.runtime.context.costTier;  // [!code highlight]
        const environment = request.runtime.context.environment;  // [!code highlight]

        let model;

        if (environment === "production" && costTier === "premium") {
          model = premiumModel;
        } else if (costTier === "budget") {
          model = budgetModel;
        } else {
          model = standardModel;
        }

        return handler({ ...request, model });  // [!code highlight]
      },
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

See [Dynamic model](/oss/javascript/langchain/agents#dynamic-model) for more examples.

### Response format

Structured output transforms unstructured text into validated, structured data. When extracting specific fields or returning data for downstream systems, free-form text isn't sufficient.

**How it works:** When you provide a schema as the response format, the model's final response is guaranteed to conform to that schema. The agent runs the model / tool calling loop until the model is done calling tools, then the final response is coerced into the provided format.

#### Defining formats

Schema definitions guide the model. Field names, types, and descriptions specify exactly what format the output should adhere to.

```typescript

const customerSupportTicket = z.object({
  category: z.enum(["billing", "technical", "account", "product"]).describe(
    "Issue category"
  ),
  priority: z.enum(["low", "medium", "high", "critical"]).describe(
    "Urgency level"
  ),
  summary: z.string().describe(
    "One-sentence summary of the customer's issue"
  ),
  customerSentiment: z.enum(["frustrated", "neutral", "satisfied"]).describe(
    "Customer's emotional tone"
  ),
}).describe("Structured ticket information extracted from customer message");

Selecting formats#

Dynamic response format selection adapts schemas based on user preferences, conversation stage, or role—returning simple formats early and detailed formats as complexity increases.

Configure structured output based on conversation state:

    import { createMiddleware } from "langchain";
    import { z } from "zod";

    const simpleResponse = z.object({
      answer: z.string().describe("A brief answer"),
    });

    const detailedResponse = z.object({
      answer: z.string().describe("A detailed answer"),
      reasoning: z.string().describe("Explanation of reasoning"),
      confidence: z.number().describe("Confidence score 0-1"),
    });

    const stateBasedOutput = createMiddleware({
      name: "StateBasedOutput",
      wrapModelCall: (request, handler) => {
        // request.state is a shortcut for request.state.messages
        const messageCount = request.messages.length;  // [!code highlight]

        let responseFormat;
        if (messageCount < 3) {
          // Early conversation - use simple format
          responseFormat = simpleResponse; // [!code highlight]
        } else {
          // Established conversation - use detailed format
          responseFormat = detailedResponse; // [!code highlight]
        }

        return handler({ ...request, responseFormat });
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Configure output format based on user preferences in Store:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    const verboseResponse = z.object({
      answer: z.string().describe("Detailed answer"),
      sources: z.array(z.string()).describe("Sources used"),
    });

    const conciseResponse = z.object({
      answer: z.string().describe("Brief answer"),
    });

    const storeBasedOutput = createMiddleware({
      name: "StoreBasedOutput",
      wrapModelCall: async (request, handler) => {
        const userId = request.runtime.context.userId;  // [!code highlight]

        // Read from Store: get user's preferred response style
        const store = request.runtime.store;  // [!code highlight]
        const userPrefs = await store.get(["preferences"], userId);  // [!code highlight]

        if (userPrefs) {
          const style = userPrefs.value?.responseStyle || "concise";
          if (style === "verbose") {
            request.responseFormat = verboseResponse;  // [!code highlight]
          } else {
            request.responseFormat = conciseResponse;  // [!code highlight]
          }
        }

        return handler(request);
      },
    });
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Configure output format based on Runtime Context like user role or environment:

```typescript
    import * as z from "zod";
    import { createMiddleware } from "langchain";

    const contextSchema = z.object({
      userRole: z.string(),
      environment: z.string(),
    });

    const adminResponse = z.object({
      answer: z.string().describe("Answer"),
      debugInfo: z.record(z.any()).describe("Debug information"),
      systemStatus: z.string().describe("System status"),
    });

    const userResponse = z.object({
      answer: z.string().describe("Answer"),
    });

    const contextBasedOutput = createMiddleware({
      name: "ContextBasedOutput",
      wrapModelCall: (request, handler) => {
        // Read from Runtime Context: user role and environment
        const userRole = request.runtime.context.userRole;  // [!code highlight]
        const environment = request.runtime.context.environment;  // [!code highlight]

        let responseFormat;
        if (userRole === "admin" && environment === "production") {
          responseFormat = adminResponse;  // [!code highlight]
        } else {
          responseFormat = userResponse;  // [!code highlight]
        }

        return handler({ ...request, responseFormat });
      },
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

## Tool context

Tools are special in that they both read and write context.

In the most basic case, when a tool executes, it receives the LLM's request parameters and returns a tool message back. The tool does its work and produces a result.

Tools can also fetch important information for the model that allows it to perform and complete tasks.

### Reads

Most real-world tools need more than just the LLM's parameters. They need user IDs for database queries, API keys for external services, or current session state to make decisions. Tools read from state, store, and runtime context to access this information.

<span class="tab-group-start"></span>
  <span class="tab-start" data-tab-title="State"></span>
Read from State to check current session information:

```typescript
    import * as z from "zod";
    import { createAgent, tool, type ToolRuntime } from "langchain";

    const checkAuthentication = tool(
      async (_, runtime: ToolRuntime) => {
        // Read from State: check current auth status
        const currentState = runtime.state;
        const isAuthenticated = currentState.authenticated || false;

        if (isAuthenticated) {
          return "User is authenticated";
        } else {
          return "User is not authenticated";
        }
      },
      {
        name: "check_authentication",
        description: "Check if user is authenticated",
        schema: z.object({}),
      }
    );
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Read from Store to access persisted user preferences:

```typescript
    import * as z from "zod";
    import { createAgent, tool, type ToolRuntime } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
    });

    const getPreference = tool(
      async ({ preferenceKey }, runtime: ToolRuntime) => {
        const userId = runtime.context.userId;

        // Read from Store: get existing preferences
        const store = runtime.store;
        const existingPrefs = await store.get(["preferences"], userId);

        if (existingPrefs) {
          const value = existingPrefs.value?.[preferenceKey];
          return value ? `${preferenceKey}: ${value}` : `No preference set for ${preferenceKey}`;
        } else {
          return "No preferences found";
        }
      },
      {
        name: "get_preference",
        description: "Get user preference from Store",
        schema: z.object({
          preferenceKey: z.string(),
        }),
      }
    );
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Runtime Context"></span>
Read from Runtime Context for configuration like API keys and user IDs:

```typescript
    import * as z from "zod";
    import { tool } from "@langchain/core/tools";
    import { createAgent } from "langchain";

    const contextSchema = z.object({
      userId: z.string(),
      apiKey: z.string(),
      dbConnection: z.string(),
    });

    const fetchUserData = tool(
      async ({ query }, runtime: ToolRuntime<any, typeof contextSchema>) => {
        // Read from Runtime Context: get API key and DB connection
        const { userId, apiKey, dbConnection } = runtime.context;

        // Use configuration to fetch data
        const results = await performDatabaseQuery(dbConnection, query, apiKey);

        return `Found ${results.length} results for user ${userId}`;
      },
      {
        name: "fetch_user_data",
        description: "Fetch data using Runtime Context configuration",
        schema: z.object({
          query: z.string(),
        }),
      }
    );

    const agent = createAgent({
      model: "gpt-4.1",
      tools: [fetchUserData],
      contextSchema,
    });
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

### Writes

Tool results can be used to help an agent complete a given task. Tools can both return results directly to the model
and update the memory of the agent to make important context available to future steps.

<span class="tab-group-start"></span>
  <span class="tab-start" data-tab-title="State"></span>
Write to State to track session-specific information using Command:

```typescript
    import * as z from "zod";
    import { tool } from "@langchain/core/tools";
    import { createAgent } from "langchain";
    import { Command } from "@langchain/langgraph";

    const authenticateUser = tool(
      async ({ password }) => {
        // Perform authentication
        if (password === "correct") {
          // Write to State: mark as authenticated using Command
          return new Command({
            update: { authenticated: true },
          });
        } else {
          return new Command({ update: { authenticated: false } });
        }
      },
      {
        name: "authenticate_user",
        description: "Authenticate user and update State",
        schema: z.object({
          password: z.string(),
        }),
      }
    );
    ```
  <span class="tab-end"></span>

  <span class="tab-start" data-tab-title="Store"></span>
Write to Store to persist data across sessions:

```typescript
    import * as z from "zod";
    import { createAgent, tool, type ToolRuntime } from "langchain";

    const savePreference = tool(
      async ({ preferenceKey, preferenceValue }, runtime: ToolRuntime<any, typeof contextSchema>) => {
        const userId = runtime.context.userId;

        // Read existing preferences
        const store = runtime.store;
        const existingPrefs = await store.get(["preferences"], userId);

        // Merge with new preference
        const prefs = existingPrefs?.value || {};
        prefs[preferenceKey] = preferenceValue;

        // Write to Store: save updated preferences
        await store.put(["preferences"], userId, prefs);

        return `Saved preference: ${preferenceKey} = ${preferenceValue}`;
      },
      {
        name: "save_preference",
        description: "Save user preference to Store",
        schema: z.object({
          preferenceKey: z.string(),
          preferenceValue: z.string(),
        }),
      }
    );
    ```
  <span class="tab-end"></span>
<span class="tab-group-end"></span>

See [Tools](/oss/javascript/langchain/tools) for comprehensive examples of accessing state, store, and runtime context in tools.

## Life-cycle context

Control what happens **between** the core agent steps - intercepting data flow to implement cross-cutting concerns like summarization, guardrails, and logging.

As you've seen in [Model Context](#model-context) and [Tool Context](#tool-context), [middleware](/oss/javascript/langchain/middleware) is the mechanism that makes context engineering practical. Middleware allows you to hook into any step in the agent lifecycle and either:

1. **Update context** - Modify state and store to persist changes, update conversation history, or save insights
2. **Jump in the lifecycle** - Move to different steps in the agent cycle based on context (e.g., skip tool execution if a condition is met, repeat model call with modified context)


  <img src="https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=eb4404b137edec6f6f0c8ccb8323eaf1" alt="Middleware hooks in the agent loop" className="rounded-lg" data-og-width="500" width="500" data-og-height="560" height="560" data-path="oss/images/middleware_final.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=280&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=483413aa87cf93323b0f47c0dd5528e8 280w, https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=560&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=41b7dd647447978ff776edafe5f42499 560w, https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=840&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=e9b14e264f68345de08ae76f032c52d4 840w, https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=1100&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=ec45e1932d1279b1beee4a4b016b473f 1100w, https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=1650&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=3bca5ebf8aa56632b8a9826f7f112e57 1650w, https://mintcdn.com/langchain-5e9cc07a/RAP6mjwE5G00xYsA/oss/images/middleware_final.png?w=2500&fit=max&auto=format&n=RAP6mjwE5G00xYsA&q=85&s=437f141d1266f08a95f030c2804691d9 2500w" />


### Example: Summarization

One of the most common life-cycle patterns is automatically condensing conversation history when it gets too long. Unlike the transient message trimming shown in [Model Context](#messages), summarization **persistently updates state** - permanently replacing old messages with a summary that's saved for all future turns.

LangChain offers built-in middleware for this:

```typescript

const agent = createAgent({
  model: "gpt-4.1",
  tools: [...],
  middleware: [
    summarizationMiddleware({
      model: "gpt-4.1-mini",
      trigger: { tokens: 4000 },
      keep: { messages: 20 },
    }),
  ],
});

When the conversation exceeds the token limit, SummarizationMiddleware automatically:

  1. Summarizes older messages using a separate LLM call
  2. Replaces them with a summary message in State (permanently)
  3. Keeps recent messages intact for context

The summarized conversation history is permanently updated - future turns will see the summary instead of the original messages.

For a complete list of built-in middleware, available hooks, and how to create custom middleware, see the Middleware documentation.

Best practices#

  1. Start simple - Begin with static prompts and tools, add dynamics only when needed
  2. Test incrementally - Add one context engineering feature at a time
  3. Monitor performance - Track model calls, token usage, and latency
  4. Use built-in middleware - Leverage SummarizationMiddleware, LLMToolSelectorMiddleware, etc.
  5. Document your context strategy - Make it clear what context is being passed and why
  6. Understand transient vs persistent: Model context changes are transient (per-call), while life-cycle context changes persist to state

Edit this page on GitHub or file an issue.

Connect these docs to Claude, VSCode, and more via MCP for real-time answers.

Link last verified June 7, 2026. View original ↗
Source: LangChain Docs
Link last verified: 2026-03-04