Skip to content

Commit c5c1ee4

Browse files
nicohrubecjadengis
andauthored
fix(core): Langgraph state graph invoke accepts null to resume (#19374)
Closes #19353 Co-Authored-By: John Dengis <jadengis@users.noreply.github.com>
1 parent 3cc0b1d commit c5c1ee4

File tree

4 files changed

+91
-2
lines changed

4 files changed

+91
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
This new mode no longer creates a session per soft navigation but continues the initial session until the next hard page refresh.
3737
Check out the [docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/browsersession/) to learn more!
3838

39-
Work in this release was contributed by @LudvigHz. Thank you for your contribution!
39+
Work in this release was contributed by @LudvigHz and @jadengis. Thank you for your contributions!
4040

4141
## 10.39.0
4242

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { END, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
2+
import * as Sentry from '@sentry/node';
3+
4+
async function run() {
5+
await Sentry.startSpan({ op: 'function', name: 'langgraph-resume-test' }, async () => {
6+
const mockLlm = () => {
7+
return {
8+
messages: [
9+
{
10+
role: 'assistant',
11+
content: 'Mock LLM response',
12+
response_metadata: {
13+
model_name: 'mock-model',
14+
finish_reason: 'stop',
15+
tokenUsage: {
16+
promptTokens: 20,
17+
completionTokens: 10,
18+
totalTokens: 30,
19+
},
20+
},
21+
},
22+
],
23+
};
24+
};
25+
26+
// Test: invoke with null input (resume after human-in-the-loop interrupt)
27+
// See: https://docs.langchain.com/oss/javascript/langgraph/use-functional-api#resuming-after-an-error
28+
const checkpointer = new MemorySaver();
29+
const graph = new StateGraph(MessagesAnnotation)
30+
.addNode('agent', mockLlm)
31+
.addEdge(START, 'agent')
32+
.addEdge('agent', END)
33+
.compile({ name: 'resume_agent', checkpointer });
34+
35+
const config = { configurable: { thread_id: 'resume-thread-1' } };
36+
await graph.invoke({ messages: [{ role: 'user', content: 'Hello' }] }, config);
37+
await graph.invoke(null, config);
38+
});
39+
40+
await Sentry.flush(2000);
41+
}
42+
43+
run();

dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,50 @@ describe('LangGraph integration', () => {
318318
});
319319
},
320320
);
321+
322+
// Test for null input resume scenario
323+
const EXPECTED_TRANSACTION_RESUME = {
324+
transaction: 'langgraph-resume-test',
325+
contexts: {
326+
trace: expect.objectContaining({
327+
status: 'ok',
328+
}),
329+
},
330+
spans: expect.arrayContaining([
331+
// create_agent span
332+
expect.objectContaining({
333+
data: {
334+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent',
335+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent',
336+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph',
337+
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'resume_agent',
338+
},
339+
description: 'create_agent resume_agent',
340+
op: 'gen_ai.create_agent',
341+
origin: 'auto.ai.langgraph',
342+
status: 'ok',
343+
}),
344+
// invoke_agent span with null input (resume)
345+
expect.objectContaining({
346+
data: expect.objectContaining({
347+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent',
348+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent',
349+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph',
350+
[GEN_AI_AGENT_NAME_ATTRIBUTE]: 'resume_agent',
351+
[GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'resume_agent',
352+
[GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'resume-thread-1',
353+
}),
354+
description: 'invoke_agent resume_agent',
355+
op: 'gen_ai.invoke_agent',
356+
origin: 'auto.ai.langgraph',
357+
status: 'ok',
358+
}),
359+
]),
360+
};
361+
362+
createEsmAndCjsTests(__dirname, 'scenario-resume.mjs', 'instrument.mjs', (createRunner, test) => {
363+
test('should not throw when invoke is called with null input (resume scenario)', async () => {
364+
await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_RESUME }).start().completed();
365+
});
366+
});
321367
});

packages/core/src/tracing/langgraph/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function instrumentCompiledGraphInvoke(
136136
const recordInputs = options.recordInputs;
137137
const recordOutputs = options.recordOutputs;
138138
const inputMessages =
139-
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] }).messages ?? []) : [];
139+
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] } | null)?.messages ?? []) : [];
140140

141141
if (inputMessages && recordInputs) {
142142
const normalizedMessages = normalizeLangChainMessages(inputMessages);

0 commit comments

Comments
 (0)