Context engineering definition and purpose
Context engineering is providing the right information and tools in the right format so the LLM can accomplish a task. It is the number one job of AI Engineers. The lack of right context is the number one blocker for more reliable agents.
Three types of context in agents
There are three types of context that you can control in agents: Model Context (what goes into model calls - instructions, message history, tools, response format; transient), Tool Context (what tools can access and produce - reads/writes to state, store, runtime context; persistent), and Life-cycle Context (what happens between model and tool calls - summarization, guardrails, logging; persistent).
Three data sources in agents
Throughout the agent process, agents access and manage three data sources: Runtime Context (also known as static configuration; conversation-scoped; examples include user ID, API keys, database connections, permissions, environment settings), State (also known as short-term memory; conversation-scoped; examples include current messages, uploaded files, authentication status, tool results), and Store (also known as long-term memory; cross-conversation; examples include user preferences, extracted insights, memories, historical data).
Transient vs persistent context updates
Transient updates modify what messages are sent to the model for a single call without changing what is saved in state, typically done with wrap_model_call. Persistent updates modify state and use either @ExtendedModelResponse with @Command from wrap_model_call layer, or life-cycle hooks like before_model, after_model, or wrap_tool_call to update conversation history.
Injecting file context from state into messages
Example: Use @wrap_model_call middleware to read uploaded_files from request.state.get('uploaded_files', []). Build file_descriptions list and prepend file context message. Create new messages list with file context appended, then use request.override(messages=messages) to inject the context before model call.
Injecting writing style from store into messages
Example: Use @wrap_model_call middleware to read writing_style from store via request.runtime.store.get(('writing_style',), user_id). Extract tone, greeting, sign_off, and example_email from stored value. Build style_context message and append to messages list (models pay more attention to final messages). Use request.override(messages=messages).
Injecting compliance rules from runtime context into messages
Example: Use @wrap_model_call middleware to read user_jurisdiction, industry, and compliance_frameworks from request.runtime.context. Build rules list based on compliance frameworks (GDPR, HIPAA) and industry type. Append compliance_context message to messages list and use request.override(messages=messages).
RAG pipeline custom workflow pattern
A common use case combining retrieval with an agent uses three types of nodes: a model node (query rewriting using structured output), a deterministic node (vector similarity search without LLM), and an agent node (reasoning over retrieved context with tool access).
RAG workflow with query rewriting Python
Example of a custom RAG workflow for WNBA stats assistant:
```python
from typing import TypedDict
from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
class State(TypedDict):
question: str
rewritten_query: str
documents: list[str]
answer: str
# WNBA knowledge base with rosters, game results, and player stats
embeddings = OpenAIEmbeddings()
vector_store = InMemoryVectorStore(embeddings)
vector_store.add_texts([
# Rosters
"New York Liberty 2024 roster: Breanna Stewart, Sabrina Ionescu, Jonquel Jones, Courtney Vandersloot.",
"Las Vegas Aces 2024 roster: A'ja Wilson, Kelsey Plum, Jackie Young, Chelsea Gray.",
"Indiana Fever 2024 roster: Caitlin Clark, Aliyah Boston, Kelsey Mitchell, NaLyssa Smith.",
# Game results
"2024 WNBA Finals: New York Liberty defeated Minnesota Lynx 3-2 to win the championship.",
"June 15, 2024: Indiana Fever 85, Chicago Sky 79. Caitlin Clark had 23 points and 8 assists.",
"August 20, 2024: Las Vegas Aces 92, Phoenix Mercury 84. A'ja Wilson scored 35 points.",
# Player stats
"A'ja Wilson 2024 season stats: 26.9 PPG, 11.9 RPG, 2.6 BPG. Won MVP award.",
"Caitlin Clark 2024 rookie stats: 19.2 PPG, 8.4 APG, 5.7 RPG. Won Rookie of the Year.",
"Breanna Stewart 2024 stats: 20.4 PPG, 8.5 RPG, 3.5 APG.",
])
retriever = vector_store.as_retriever(search_kwargs={"k": 5})
@tool
def get_latest_news(query: str) -> str:
"""Get the latest WNBA news and updates."""
# Your news API here
return "Latest: The WNBA announced expanded playoff format for 2025..."
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_latest_news],
)
model = ChatOpenAI(model="gpt-5.5")
class RewrittenQuery(BaseModel):
query: str
def rewrite_query(state: State) -> dict:
"""Rewrite the user query for better retrieval."""
system_prompt = """Rewrite this query to retrieve relevant WNBA information.
The knowledge base contains: team rosters, game results with scores, and player statistics (PPG, RPG, APG).
Focus on specific player names, team names, or stat categories mentioned."""
response = model.with_structured_output(RewrittenQuery).invoke([
{"role": "system", "content": system_prompt},
{"role": "user", "content": state["question"]}
])
return {"rewritten_query": response.query}
def retrieve(state: State) -> dict:
"""Retrieve documents based on the rewritten query."""
docs = retriever.invoke(state["rewritten_query"])
return {"documents": [doc.page_content for doc in docs]}
def call_agent(state: State) -> dict:
"""Generate answer using retrieved context."""
context = "\n\n".join(state["documents"])
prompt = f"Context:\n{context}\n\nQuestion: {state['question']}"
response = agent.invoke({"messages": [{"role": "user", "content": prompt}]})
return {"answer": response["messages"][-1].content_blocks}
workflow = (
StateGraph(State)
.add_node("rewrite", rewrite_query)
.add_node("retrieve", retrieve)
.add_node("agent", call_agent)
.add_edge(START, "rewrite")
.add_edge("rewrite", "retrieve")
.add_edge("retrieve", "agent")
.add_edge("agent", END)
.compile()
)
result = workflow.invoke({"question": "Who won the 2024 WNBA Championship?"})
print(result["answer"])
```
RAG workflow with query rewriting TypeScript
Example of a custom RAG workflow for WNBA stats assistant:
```typescript
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import { createAgent, tool } from "langchain";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import * as z from "zod";
const State = Annotation.Root({
question: Annotation<string>(),
rewrittenQuery: Annotation<string>(),
documents: Annotation<string[]>(),
answer: Annotation<string>(),
});
// WNBA knowledge base with rosters, game results, and player stats
const embeddings = new OpenAIEmbeddings();
const vectorStore = await MemoryVectorStore.fromTexts(
[
// Rosters
"New York Liberty 2024 roster: Breanna Stewart, Sabrina Ionescu, Jonquel Jones, Courtney Vandersloot.",
"Las Vegas Aces 2024 roster: A'ja Wilson, Kelsey Plum, Jackie Young, Chelsea Gray.",
"Indiana Fever 2024 roster: Caitlin Clark, Aliyah Boston, Kelsey Mitchell, NaLyssa Smith.",
// Game results
"2024 WNBA Finals: New York Liberty defeated Minnesota Lynx 3-2 to win the championship.",
"June 15, 2024: Indiana Fever 85, Chicago Sky 79. Caitlin Clark had 23 points and 8 assists.",
"August 20, 2024: Las Vegas Aces 92, Phoenix Mercury 84. A'ja Wilson scored 35 points.",
// Player stats
"A'ja Wilson 2024 season stats: 26.9 PPG, 11.9 RPG, 2.6 BPG. Won MVP award.",
"Caitlin Clark 2024 rookie stats: 19.2 PPG, 8.4 APG, 5.7 RPG. Won Rookie of the Year.",
"Breanna Stewart 2024 stats: 20.4 PPG, 8.5 RPG, 3.5 APG.",
],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
embeddings
);
const retriever = vectorStore.asRetriever({ k: 5 });
const getLatestNews = tool(
async ({ query }) => {
// Your news API here
return "Latest: The WNBA announced expanded playoff format for 2025...";
},
{
name: "get_latest_news",
description: "Get the latest WNBA news and updates",
schema: z.object({ query: z.string() }),
}
);
const agent = createAgent({
model: "openai:gpt-5.5",
tools: [getLatestNews],
});
const model = new ChatOpenAI({ model: "gpt-5.5" });
const RewrittenQuery = z.object({ query: z.string() });
async function rewriteQuery(state: typeof State.State) {
const systemPrompt = `Rewrite this query to retrieve relevant WNBA information.
The knowledge base contains: team rosters, game results with scores, and player statistics (PPG, RPG, APG).
Focus on specific player names, team names, or stat categories mentioned.`;
const response = await model.withStructuredOutput(RewrittenQuery).invoke([
{ role: "system", content: systemPrompt },
{ role: "user", content: state.question },
]);
return { rewrittenQuery: response.query };
}
async function retrieve(state: typeof State.State) {
const docs = await retriever.invoke(state.rewrittenQuery);
return { documents: docs.map((doc) => doc.pageContent) };
}
async function callAgent(state: typeof State.State) {
const context = state.documents.join("\n\n");
const prompt = `Context:\n${context}\n\nQuestion: ${state.question}`;
const response = await agent.invoke({
messages: [{ role: "user", content: prompt }],
});
return { answer: response.messages.at(-1)?.contentBlocks };
}
const workflow = new StateGraph(State)
.addNode("rewrite", rewriteQuery)
.addNode("retrieve", retrieve)
.addNode("agent", callAgent)
.addEdge(START, "rewrite")
.addEdge("rewrite", "retrieve")
.addEdge("retrieve", "agent")
.addEdge("agent", END)
.compile();
const result = await workflow.invoke({
question: "Who won the 2024 WNBA Championship?",
});
console.log(result.answer);
```
Context engineering is central to multi-agent design
At the center of multi-agent design is context engineering—deciding what information each agent sees. The quality of your system depends on ensuring each agent has access to the right data for its task.
Progressive disclosure for agents
Progressive disclosure is a context management technique where agents load information on-demand rather than upfront. This technique uses a three-level architecture: metadata (skill descriptions in system prompt) → core content (loaded via tool calls) → detailed resources (full database schemas and business logic). Progressive disclosure reduces context usage by loading only the 2-3 skills needed for a task, enables team autonomy for specialized skills development, scales efficiently to dozens or hundreds of skills, and simplifies conversation history with a single agent and one conversation thread.
Progressive disclosure trade-offs
Progressive disclosure has two main trade-offs: (1) Latency - loading skills on-demand requires additional tool calls, which adds latency to the first request that needs each skill; (2) Workflow control - basic implementations rely on prompting to guide skill usage, and you cannot enforce hard constraints like 'always try skill A before skill B' without custom logic. However, custom state tracking can enable tool constraints after specific skills have been loaded.
Skill storage and discovery implementations
When building a skills implementation, there are flexible options for different components: Storage can use databases, S3, in-memory data structures, or any backend. Discovery can use direct lookup (as in the tutorial), RAG for large skill collections, file system scanning, or API calls. Loading logic can be customized to search through skill content, rank relevance, or load progressively. Side effects can be defined for when skills load, such as exposing tools associated with that skill. The choice of implementation depends on requirements around performance, storage, and workflow control.
Skill sizing guidelines for context windows
When deciding whether to use progressive disclosure or include skills directly in the system prompt, consider skill size: Small skills (less than 1K tokens, approximately 750 words) can be included directly in the system prompt and potentially cached with prompt caching for cost savings and faster responses. Medium skills (1-10K tokens, approximately 750-7,500 words) benefit from on-demand loading to avoid context overhead. Large skills (greater than 10K tokens, approximately 7,500 words, or greater than 5-10% of context window) should use progressive disclosure techniques like pagination, search-based loading, or hierarchical exploration to avoid consuming excessive context.
Progressive disclosure and RAG relationship
Skills with progressive disclosure can be viewed as a form of RAG (Retrieval-Augmented Generation), where each skill is a retrieval unit. However, skills are not necessarily backed by embeddings or keyword search. Instead, they use tools for browsing content (like file operations or direct lookup) to retrieve information on-demand. This pattern combines retrieval (loading skill content) with generation (using the loaded content to write queries or responses).
Progressive disclosure combined with few-shot prompting
Progressive disclosure can be extended to load dynamic few-shot examples that match the user's query. For a SQL query assistant: (1) User asks a question requiring specific knowledge, (2) Agent loads the relevant skill schema (e.g., sales_analytics), (3) Agent also loads 2-3 relevant example queries via semantic search or tag-based lookup matching the user's intent, (4) Agent writes the query using both schema knowledge and example patterns. This combination creates a powerful context engineering pattern that scales to large knowledge bases while providing high-quality, grounded outputs.
Context engineering for subagents
Control how context flows between the main agent and its subagents across three categories: (1) Subagent specs—ensure subagents are invoked when they should be, impacting main agent routing decisions; (2) Subagent inputs—ensure subagents can execute well with optimized context, impacting subagent performance; (3) Subagent outputs—ensure the supervisor can act on subagent results, impacting main agent performance.
Subagent specs: names and descriptions as prompting levers
The names and descriptions associated with subagents are the primary way the main agent knows which subagents to invoke. These are prompting levers—choose them carefully. Name should be how the main agent refers to the sub-agent, kept clear and action-oriented (e.g., 'research_agent', 'code_reviewer'). Description should convey what the main agent knows about the sub-agent's capabilities, being specific about what tasks it handles and when to use it.
Single dispatch tool: agent discovery methods
For the single dispatch tool design, provide the main agent with information about the subagents it can invoke. You can provide this information in different ways based on number of agents and whether registry is static or dynamic: (1) System prompt enumeration—best for small, static agent lists (< 10 agents), simple but requires prompt updates when agents change; (2) Enum constraint—best for small, static agent lists (< 10 agents), type-safe and explicit but requires code changes when agents change; (3) Tool-based discovery—best for large or dynamic agent registries, flexible and scalable but adds complexity.
System prompt enumeration for agent discovery
List available agents directly in the main agent's system prompt. The main agent sees the list of agents and their descriptions as part of its instructions. Use this when you have a small, fixed set of agents (< 10), agent registry rarely changes, or you want the simplest implementation. Example: system_prompt with enumerated list of available agents and their descriptions.
Enum constraint for agent discovery
Add an enum constraint to the 'agent_name' parameter in your dispatch tool. This provides type safety and makes available agents explicit in the tool schema. Use this when you have a small, fixed set of agents (< 10), want type safety and explicit agent names, or prefer schema-based validation over prompt-based guidance. This requires importing Enum class and defining an AgentName enum with each agent as a string enum value.
Tool-based discovery for agent discovery
Provide a separate tool (e.g., 'list_agents' or 'search_agents') that the main agent can call to discover available agents on-demand. This enables progressive disclosure and supports dynamic registries. Use this when you have many agents (> 10) or a growing registry, agent registry changes frequently or is dynamic, you want to reduce prompt size and token usage, or different teams manage different agents independently.
Subagent inputs: customizing context and state
Customize what context the subagent receives to execute its task. Add input that is not practical to capture in a static prompt—full message history, prior results, or task metadata—by pulling from the agent's state. Use ToolRuntime to access the main agent's state and pass relevant context to the subagent's invoke call, including custom state keys that are defined in both main and subagent state schemas.
Subagent inputs example (Python)
from langchain.agents import AgentState
from langchain.tools import tool, ToolRuntime
class CustomState(AgentState):
example_state_key: str
@tool(
"subagent1_name",
description="subagent1_description"
)
def call_subagent1(query: str, runtime: ToolRuntime[None, CustomState]):
# Apply any logic needed to transform the messages into a suitable input
subagent_input = some_logic(query, runtime.state["messages"])
result = subagent1.invoke({
"messages": subagent_input,
# You could also pass other state keys here as needed.
# Make sure to define these in both the main and subagent's
# state schemas.
"example_state_key": runtime.state["example_state_key"]
})
return result["messages"][-1].content
Subagent inputs example (TypeScript)
import { createAgent, tool, AgentState, ToolMessage } from "langchain";
import { Command } from "@langchain/langgraph";
import * as z from "zod";
// Example of passing the full conversation history to the sub agent via the state.
const callSubagent1 = tool(
async ({query}) => {
const state = getCurrentTaskInput<AgentState>();
// Apply any logic needed to transform the messages into a suitable input
const subAgentInput = someLogic(query, state.messages);
const result = await subagent1.invoke({
messages: subAgentInput,
// You could also pass other state keys here as needed.
// Make sure to define these in both the main and subagent's
// state schemas.
exampleStateKey: state.exampleStateKey
});
return result.messages.at(-1)?.content;
},
{
name: "subagent1_name",
description: "subagent1_description",
}
);
Subagent outputs: customizing supervisor feedback
Customize what the main agent receives back so it can make good decisions. Two strategies: (1) Prompt the sub-agent—specify exactly what should be returned. A common failure mode is that the sub-agent performs tool calls or reasoning but does not include results in its final message—remind it that the supervisor only sees the final output. (2) Format in code—adjust or enrich the response before returning it. For example, pass specific state keys back in addition to the final text using a Command.
Subagent outputs example (Python)
from typing import Annotated
from langchain.agents import AgentState
from langchain.tools import InjectedToolCallId
from langgraph.types import Command
@tool(
"subagent1_name",
description="subagent1_description"
)
def call_subagent1(
query: str,
tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
result = subagent1.invoke({
"messages": [{"role": "user", "content": query}]
})
return Command(update={
# Pass back additional state from the subagent
"example_state_key": result["example_state_key"],
"messages": [
ToolMessage(
content=result["messages"][-1].content,
tool_call_id=tool_call_id
)
]
})
Subagent outputs example (TypeScript)
import { tool, ToolMessage } from "langchain";
import { Command } from "@langchain/langgraph";
import * as z from "zod";
const callSubagent1 = tool(
async ({ query }, config) => {
const result = await subagent1.invoke({
messages: [{ role: "user", content: query }]
});
// Return a Command to update multiple state keys
return new Command({
update: {
// Pass back additional state from the subagent
exampleStateKey: result.exampleStateKey,
messages: [
new ToolMessage({
content: result.messages.at(-1)?.text,
tool_call_id: config.toolCall?.id!
})
]
}
});
},
{
name: "subagent1_name",
description: "subagent1_description",
schema: z.object({
query: z.string().describe("The query to send to subagent1")
})
}
);
Context window limitations with long conversations
Long conversations pose a challenge to LLMs because the full history may not fit inside an LLM's context window, resulting in context loss or errors. Even if the model supports the full context length, most LLMs still perform poorly over long contexts; they get distracted by stale or off-topic content while suffering from slower response times and higher costs.
ToolRuntime provides access to state, context, store, and execution info
Add runtime: ToolRuntime to your tool signature to access state (short-term memory for current conversation), context (immutable configuration passed at invocation time), store (long-term persistent memory across conversations), stream_writer (emit real-time updates), execution_info (thread ID, run ID, attempt number), and server_info (assistant ID, graph ID, authenticated user when running on LangGraph Server). The runtime parameter is automatically injected and hidden from the LLM.
Access tool state with ToolRuntime
Tools can access the current conversation state using runtime.state, which includes message history and custom fields defined in the graph state. Access messages with runtime.state['messages'] and custom fields with runtime.state.get('field_name').
Update tool state with Command
Use Command to update the agent's state from within a tool. Include a ToolMessage in the update so the model can see the result of the tool call. Use runtime.tool_call_id for the tool_call_id parameter.
Tool context is immutable per-run configuration
Context provides immutable configuration data passed at invocation time, used for user IDs, session details, or application-specific settings that shouldn't change during a conversation. Access context through runtime.context.
Difference between thread_id and context
thread_id (passed via config={'configurable': {'thread_id': ...}}) scopes the conversation with persistent message history and checkpoints. context carries per-run data your tools and middleware read at invocation time. In production, pass both together: a stable thread_id per conversation and a context object on every invoke.
Tool store for long-term memory
Access the store through runtime.store for persistent data that survives across conversations. The store uses a namespace/key pattern (e.g., store.get(('users',), user_id) and store.put(('users',), user_id, data)) to organize data.
Persistent store implementations for production
For production deployments, use persistent store implementations like PostgresStore, MongoDBStore, or RedisStore instead of InMemoryStore.
Tool execution info contains thread and run IDs
Access thread ID, run ID, and retry state from within a tool via runtime.execution_info. Properties include thread_id, run_id, and node_attempt.
Tool server info for LangGraph Server
When a tool runs on LangGraph Server, access the assistant ID, graph ID, and authenticated user via runtime.server_info. Properties include assistant_id, graph_id, and user.identity. server_info is None when not running on LangGraph Server.
Accessing full thread context in supervisor tool
When wrapping sub-agents as tools in a supervisor, you can access the full conversation state via ToolRuntime (Python) or getCurrentTaskInput (TypeScript) to pass additional context to sub-agents. This allows sub-agents to see the full conversation context, which can help resolve ambiguities like 'schedule it for the same time tomorrow' (referencing previous conversation).
Customizing output from sub-agent tools
Sub-agent tools can customize what information flows back to the supervisor. Options include returning just the confirmation message (string), or returning structured data as JSON. The choice depends on what the supervisor needs to synthesize results and respond to the user.