Overview
In a UI-embedded agent chat, each user message often carries app context: the document that was open, the field that was selected, etc. You want that context available to the LLM so it can resolve references like "what is this document about?" or "make it shorter", but you don't want to bloat thread history with the full content.
The supported pattern in LangChain.js has two parts:
Store compact context (IDs) on the message via
additional_kwargs.Expand those IDs to text at model-call time using a
wrapModelCallmiddleware. History stays small, the LLM gets the full context.
Step 1: Attach metadata with additional_kwargs
HumanMessage.additional_kwargs is the right place for developer-owned per-message metadata. It is serialization-safe, so it survives LangGraph checkpoint/resume and thread history storage.
new HumanMessage({
content: "what is this document about",
additional_kwargs: { appContext: { documentId, fieldId } },
});Namespace your data under a single key (here, appContext). LLM integrations also write into additional_kwargs on AI messages (for example OpenAI's tool_calls and function_call), so a namespace prevents collisions.
Note: additional_kwargs is not forwarded to the LLM automatically. Storing data there persists it in history but does not put it in the prompt. That is what step 2 is for.
Step 2: Expand IDs to text with wrapModelCall middleware
Use createMiddleware with a wrapModelCall hook to transform messages right before they are sent to the model. Resolve IDs into text and inline them into the message content. The stored history is untouched.
const injectContext = createMiddleware({
name: "InjectMessageContext",
wrapModelCall: (request, handler) => {
const messages = request.messages.map((m) => {
const ctx = m.additional_kwargs?.appContext;
if (!ctx) return m;
const resolved = resolveContext(ctx); // developer-supplied: IDs → text
return new HumanMessage({ content: `${m.content}\n\n[Context: ${resolved}]` });
});
return handler({ ...request, messages });
},
});Properties of this pattern
Non-destructive: only the
messagespassed to the model are rewritten. Persisted history keeps the compact IDs.Per-message: each message can carry its own context payload, so different turns in the same thread can reference different documents or fields.
Developer-controlled resolution:
resolveContextis your function. Look up the document from an API, local state, a cache, whatever fits.Composable: stack it with other middleware.
References
Messages (LangChain.js) —
HumanMessageandadditional_kwargs.Custom middleware (LangChain.js) —
createMiddlewareandwrapModelCall.