Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/tracing/langgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function instrumentCompiledGraphInvoke(
const recordInputs = options.recordInputs;
const recordOutputs = options.recordOutputs;
const inputMessages =
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] }).messages ?? []) : [];
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] } | null)?.messages ?? []) : [];

if (inputMessages && recordInputs) {
const normalizedMessages = normalizeLangChainMessages(inputMessages);
Expand Down
85 changes: 85 additions & 0 deletions packages/core/test/tracing/langgraph-invoke-null-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it, vi } from 'vitest';
import { instrumentLangGraph } from '../../src/tracing/langgraph';

/**
* Creates a minimal mock StateGraph that simulates LangGraph's StateGraph.
* The compile() method returns a mock CompiledGraph with an invoke() method.
*/
function createMockStateGraph(invokeResult: unknown = { messages: [] }) {
return {
compile: (options?: Record<string, unknown>) => {
return {
invoke: vi.fn().mockResolvedValue(invokeResult),
name: options?.name ?? 'test_graph',
builder: {
nodes: {},
},
};
},
};
}

describe('LangGraph invoke with null input (resume scenario)', () => {
it('should not throw TypeError when invoke is called with null as first argument', async () => {
const stateGraph = createMockStateGraph();
instrumentLangGraph(stateGraph, { recordInputs: true, recordOutputs: true });

const compiled = stateGraph.compile({ name: 'resume_agent' });

// Simulates graph.invoke(null, config) which is the standard pattern
// for resuming a LangGraph graph after a human-in-the-loop interrupt.
// Previously this would throw: TypeError: Cannot read properties of null (reading 'messages')
await expect(
compiled.invoke(null, {
configurable: { thread_id: 'thread-123' },
}),
).resolves.not.toThrow();
});

it('should not throw TypeError when invoke is called with undefined as first argument', async () => {
const stateGraph = createMockStateGraph();
instrumentLangGraph(stateGraph, { recordInputs: true, recordOutputs: true });

const compiled = stateGraph.compile({ name: 'resume_agent' });

await expect(
compiled.invoke(undefined, {
configurable: { thread_id: 'thread-123' },
}),
).resolves.not.toThrow();
});

it('should not throw when invoke is called with no arguments', async () => {
const stateGraph = createMockStateGraph();
instrumentLangGraph(stateGraph, { recordInputs: true, recordOutputs: true });

const compiled = stateGraph.compile({ name: 'resume_agent' });

await expect(compiled.invoke()).resolves.not.toThrow();
});

it('should still work correctly with a normal messages input', async () => {
const mockResult = {
messages: [
{ role: 'user', content: 'hello' },
{ role: 'assistant', content: 'Hi there!' },
],
};
const stateGraph = createMockStateGraph(mockResult);
instrumentLangGraph(stateGraph, { recordInputs: true, recordOutputs: true });

const compiled = stateGraph.compile({ name: 'chat_agent' });

const result = await compiled.invoke({ messages: [{ role: 'user', content: 'hello' }] });
expect(result).toEqual(mockResult);
});

it('should still work correctly with an empty object input', async () => {
const stateGraph = createMockStateGraph();
instrumentLangGraph(stateGraph, { recordInputs: true, recordOutputs: true });

const compiled = stateGraph.compile({ name: 'resume_agent' });

await expect(compiled.invoke({}, { configurable: { thread_id: 'thread-456' } })).resolves.not.toThrow();
});
});