@@ -5,9 +5,10 @@ import { ChatOpenAI } from "@langchain/openai";
55import { HumanMessage , AIMessage } from "@langchain/core/messages" ;
66import { MemorySaver } from "@langchain/langgraph-checkpoint" ;
77import { Command } from "@langchain/langgraph" ;
8+ import { ToolMessage } from "@langchain/core/messages" ;
89
910import { tool } from "@langchain/core/tools" ;
10- import { createAgent } from "../../index.js" ;
11+ import { createAgent , type Interrupt } from "../../index.js" ;
1112import {
1213 type HITLRequest ,
1314 type HITLResponse ,
@@ -299,7 +300,6 @@ describe("humanInTheLoopMiddleware", () => {
299300 decisions : [
300301 {
301302 type : "reject" ,
302- message : "The calculation result is 500 (custom override)" ,
303303 } ,
304304 {
305305 type : "approve" ,
@@ -309,10 +309,127 @@ describe("humanInTheLoopMiddleware", () => {
309309 } ) ,
310310 thread
311311 ) ;
312- expect ( resume . structuredResponse ) . toEqual ( {
312+
313+ /**
314+ * we expect another interrupt as model updates the tool call
315+ */
316+ expect ( "__interrupt__" in resume ) . toBe ( true ) ;
317+
318+ const lastMessage = resume . messages . at ( - 1 ) as AIMessage ;
319+ const finalResume = await agent . invoke (
320+ new Command ( {
321+ resume : {
322+ decisions :
323+ lastMessage . tool_calls ?. map ( ( ) => ( {
324+ type : "approve" ,
325+ } ) ) ?? [ ] ,
326+ } satisfies HITLResponse ,
327+ } ) ,
328+ thread
329+ ) ;
330+
331+ expect ( finalResume . structuredResponse ) . toEqual ( {
313332 result : expect . toBeOneOf ( [ 500 , 579 ] ) ,
314333 name : "Thomas" ,
315334 } ) ;
335+ /**
336+ * we expect the final resume to have 8 messages:
337+ * 1. human message
338+ * 2. AI message with 2 calls
339+ * 3. Rejected tool message
340+ * 4. new tool call
341+ * 5. approved tool message
342+ * 6. approved tool message
343+ * 7. AI message with final response
344+ */
345+ expect ( finalResume . messages ) . toHaveLength ( 7 ) ;
346+ } ) ;
347+
348+ it ( "should allow to reject tool calls and give model feedback" , async ( ) => {
349+ const checkpointer = new MemorySaver ( ) ;
350+ const sendEmailTool = tool (
351+ ( ) => {
352+ return "Email sent!" ;
353+ } ,
354+ {
355+ name : "send_email" ,
356+ description : "Sends an email" ,
357+ schema : z . object ( {
358+ message : z . string ( ) ,
359+ to : z . array ( z . string ( ) ) ,
360+ subject : z . string ( ) ,
361+ } ) ,
362+ }
363+ ) ;
364+ const agent = createAgent ( {
365+ model,
366+ middleware : [
367+ humanInTheLoopMiddleware ( {
368+ interruptOn : {
369+ send_email : true ,
370+ } ,
371+ } ) ,
372+ ] as const ,
373+ tools : [ sendEmailTool ] ,
374+ checkpointer,
375+ } ) ;
376+
377+ const result = await agent . invoke (
378+ {
379+ messages : [
380+ new HumanMessage (
381+ "Send an email to [email protected] , saying hello!" 382+ ) ,
383+ ] ,
384+ } ,
385+ thread
386+ ) ;
387+
388+ /**
389+ * first interception
390+ */
391+ expect ( "__interrupt__" in result ) . toBe ( true ) ;
392+ const resume = await agent . invoke (
393+ new Command ( {
394+ resume : {
395+ decisions : [
396+ {
397+ type : "reject" ,
398+ message :
399+ "Send the email speaking like a pirate starting the message with 'Arrr, matey!'" ,
400+ } ,
401+ ] ,
402+ } satisfies HITLResponse ,
403+ } ) ,
404+ thread
405+ ) ;
406+
407+ /**
408+ * second interception, verify model as updated the tool call and approve
409+ */
410+ const interrupt = resume . __interrupt__ ?. [ 0 ] as Interrupt < HITLRequest > ;
411+ expect (
412+ interrupt ?. value ?. actionRequests [ 0 ] . args . message . startsWith (
413+ "Arrr, matey!"
414+ )
415+ ) . toBe ( true ) ;
416+ const finalResume = await agent . invoke (
417+ new Command ( {
418+ resume : {
419+ decisions : [
420+ {
421+ type : "approve" ,
422+ } ,
423+ ] ,
424+ } satisfies HITLResponse ,
425+ } ) ,
426+ thread
427+ ) ;
428+ const toolMessage = [ ...finalResume . messages ]
429+ . reverse ( )
430+ . find ( ToolMessage . isInstance ) ;
431+ expect ( toolMessage ) . toBeDefined ( ) ;
432+ expect ( toolMessage ?. content ) . toBe ( "Email sent!" ) ;
316433 } ) ;
317434 } ) ;
318435} ) ;
0 commit comments