JuliusBrussee/caveman · error · Error
LangGraph middleware requires configurable.thread_id
Error message
LangGraph middleware requires configurable.thread_id
What it means
scopeFromConfig derives the Caveman Scope (namespace/session/branch/cache epoch) from a LangChain RunnableConfig. LangGraph identifies a conversation thread via configurable.thread_id; without it the middleware cannot scope caching or compression, so it throws. This is a hard requirement for every LangGraph-scoped call.
Solutions
- Pass configurable.thread_id in the invocation config: model.invoke(input, { configurable: { thread_id: 'my-thread' } }).
- If using LangGraph, configure a checkpointer (e.g. MemorySaver) so thread_id is threaded through every call.
- Coerce numeric/other thread identifiers to non-empty strings before invoking.
- If invoking outside a thread context, supply an explicit synthetic thread_id per logical session.
Example fix
// before
await graph.invoke({ messages }); // no thread_id
// after
await graph.invoke({ messages }, { configurable: { thread_id: 'session-42' } }); Defensive patterns
Strategy: validation
Validate before calling
function requireThreadId(config?: RunnableConfig): string {
const tid = config?.configurable?.thread_id;
if (typeof tid !== 'string' || !tid) throw new Error('configurable.thread_id is required');
return tid;
} Type guard
const hasThreadId = (c?: RunnableConfig): c is RunnableConfig & { configurable: { thread_id: string } } =>
typeof c?.configurable?.thread_id === 'string' && c.configurable.thread_id !== ''; Try / catch
try {
await graph.invoke(input, config);
} catch (e) {
if (e instanceof Error && e.message === 'LangGraph middleware requires configurable.thread_id') {
await graph.invoke(input, { ...config, configurable: { ...config?.configurable, thread_id: newThreadId() } });
} else throw e;
} Prevention
- Always pass { configurable: { thread_id } } on every invoke
- Configure a LangGraph checkpointer so thread_id propagates
- Generate a stable thread id per logical session before invoking
- Type your config helper to require thread_id at compile time
When it happens
Trigger: Calling scopeFromConfig (directly or via resolveLangChainScope/adapters) with config.configurable undefined, or configurable.thread_id not a non-empty string.
Common situations: Invoking a LangGraph graph without passing { configurable: { thread_id } }, calling the model standalone (outside a checkpointer-managed graph), or passing thread_id as a number.
Understand the failure class
Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.
Related errors
- Invalid caveman_branch_id
- Invalid caveman_cache_epoch
- LangGraph middleware requires a nonempty…
- : MCP ownership journal failed; native config may already…
- MCP transaction failed and rolled back
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/4a60c2849a9b9fd9.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/typescript/src/langchain.ts:23
import type { JSONSchema } from '@langchain/core/utils/json_schema';
import { BaseDocumentCompressor } from '@langchain/core/retrievers/document_compressors';
import { Document, type DocumentInterface } from '@langchain/core/documents';
import { MiddlewareRuntime, recoveryInputSchema, recoveryToolDescription, type Candidate, type RecoveryBinding, type RetrieveArgs, type Scope, type Usage } from '@caveman-ai/sdk/middleware';
import { currentOwner, manifest, observe, plain, withOwner, type Attempt } from './common.js';
import { adapterCompatible, frameworkVersion } from './compatibility.js';
export type LangChainScope = Scope | ((config: RunnableConfig) => Scope);
export interface LangChainOptions { runtime: MiddlewareRuntime; scope: LangChainScope }
export interface LangChainDocumentOptions extends LangChainOptions {
/** The runtime-owned reader already registered by the application for this scope. */
sourceExpansion?: RecoveryBinding;
}
export const langChainAdapter = { id:'langchain', version:'0.1.0', framework_version:frameworkVersion('langchain')??'unknown', serialization_revision:'langchain-message-v1' };
export const langChainSupported=(_runtime:MiddlewareRuntime)=>adapterCompatible('langchain');
export function scopeFromConfig(config:RunnableConfig, namespace:string):Scope{
const c=config.configurable??{};
if(typeof c.thread_id!=='string'||!c.thread_id)throw new Error('LangGraph middleware requires configurable.thread_id');
if(c.caveman_branch_id!==undefined&&typeof c.caveman_branch_id!=='string')throw new Error('Invalid caveman_branch_id');
if(c.caveman_cache_epoch!==undefined&&typeof c.caveman_cache_epoch!=='string')throw new Error('Invalid caveman_cache_epoch');
return {namespace,session_id:c.thread_id,branch_id:c.caveman_branch_id??'main',cache_epoch:c.caveman_cache_epoch??'0'};
}
export function resolveLangChainScope(source:LangChainScope,config?:RunnableConfig):Scope{
return typeof source==='function'?source(ensureConfig(config)):source;
}
const number=(value:unknown):number|null=>typeof value==='number'&&Number.isSafeInteger(value)&&value>=0?value:null;
export function langChainUsage(value:UsageMetadata|undefined):Usage|null{
if(!value)return null;
const input=number(value.input_tokens),output=number(value.output_tokens);
return {provenance:'client_observed_sdk',complete:input!==null&&output!==null,input_tokens:input,output_tokens:output,
cache_read_tokens:number(value.input_token_details?.cache_read),cache_write_tokens:number(value.input_token_details?.cache_creation),reasoning_tokens:number(value.output_token_details?.reasoning)};
}
/** Clone only native ToolMessage text. Other message classes stay identical. */
export async function prepareLangChain(messages:BaseMessage[], options:LangChainOptions, config?:RunnableConfig, binding?:RecoveryBinding|null, prefix:BaseMessage[]=[]):Promise<{messages:BaseMessage[];attempt:Attempt|null}>{
const signal=config?.signal;signal?.throwIfAborted();View on GitHub (pinned to 3ee70a1026)