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

  1. Pass configurable.thread_id in the invocation config: model.invoke(input, { configurable: { thread_id: 'my-thread' } }).
  2. If using LangGraph, configure a checkpointer (e.g. MemorySaver) so thread_id is threaded through every call.
  3. Coerce numeric/other thread identifiers to non-empty strings before invoking.
  4. 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

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


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)