Skip to content

Commit 7a04bd1

Browse files
authored
fix: langgraph interrupt in chat (CopilotKit#1642)
1 parent d6c695b commit 7a04bd1

7 files changed

Lines changed: 54 additions & 21 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@copilotkit/react-core": patch
3+
"@copilotkit/runtime": patch
4+
"@copilotkit/sdk-js": patch
5+
---
6+
7+
- fix: fix how results are communicated back on interrupt
8+
- fix: do not allow followup for interrupt actions
9+
- chore: improve TS docs for interrupt

CopilotKit/packages/react-core/src/hooks/use-chat.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,10 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
383383
setRunId(runIdRef.current);
384384
setExtensions(extensionsRef.current);
385385
let rawMessagesResponse = value.generateCopilotResponse.messages;
386-
(value.generateCopilotResponse?.metaEvents ?? []).forEach((ev) => {
386+
387+
const metaEvents: MetaEvent[] | undefined =
388+
value.generateCopilotResponse?.metaEvents ?? [];
389+
(metaEvents ?? []).forEach((ev) => {
387390
if (ev.name === MetaEventName.LangGraphInterruptEvent) {
388391
let eventValue = langGraphInterruptEvent(ev as LangGraphInterruptEvent).value;
389392
eventValue = parseJson(eventValue, eventValue);
@@ -548,7 +551,8 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
548551
action: FrontendAction<any>,
549552
message: ActionExecutionMessage,
550553
) => {
551-
followUp = action?.followUp;
554+
const isInterruptAction = interruptMessages.find((m) => m.id === message.id);
555+
followUp = action?.followUp || !isInterruptAction;
552556
const resultMessage = await executeAction({
553557
onFunctionCall,
554558
previousMessages,

CopilotKit/packages/runtime/src/lib/runtime/remote-lg-action.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ async function streamEvents(controller: ReadableStreamDefaultController, args: E
190190
(ev) => ev.name === MetaEventName.LangGraphInterruptEvent,
191191
);
192192
if (activeInterruptEvent && !lgInterruptMetaEvent) {
193-
payload.command = { resume: formattedMessages[formattedMessages.length - 1] };
193+
// state.messages includes only messages that were not processed by the agent, which are the interrupt messages
194+
payload.command = { resume: state.messages };
194195
}
195196
if (lgInterruptMetaEvent?.response) {
196197
let response = lgInterruptMetaEvent.response;

CopilotKit/packages/sdk-js/src/langgraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,10 @@ export function copilotKitInterrupt({
303303
__copilotkit_interrupt_value__: interruptValues,
304304
__copilotkit_messages__: [interruptMessage],
305305
});
306-
answer = response.content;
306+
answer = response[response.length - 1].content;
307307

308308
return {
309309
answer,
310-
messages: [interruptMessage, response],
310+
messages: response,
311311
};
312312
}

docs/content/docs/coagents/human-in-the-loop/interrupt-flow.mdx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ We're going to have the agent ask us to name it, so we'll need a state property
247247
# Interrupt and wait for the user to respond with a name
248248
answer, messages = copilotkit_interrupt(message='Before we start, what would you like to call me?') # [!code highlight]
249249
state["agent_name"] = answer
250+
state["messages"] = [*state["messages"], *messages]
250251

251252
# Tell the agent its name
252253
system_message = SystemMessage(
@@ -260,7 +261,8 @@ We're going to have the agent ask us to name it, so we'll need a state property
260261

261262
return {
262263
**state,
263-
"messages": response,
264+
# messages receives from the interrupt are not automatically saved to state, don't forget to add them!
265+
"messages": [*state["messages"], response],
264266
}
265267
```
266268
</Tab>
@@ -275,6 +277,7 @@ We're going to have the agent ask us to name it, so we'll need a state property
275277
if (!state.agentName) {
276278
const { answer, messages } = copilotKitInterrupt({ message: 'Before we start, what would you like to call me?' }); // [!code highlight]
277279
state.agentName = answer
280+
state.messages = [...state.message, ...messages]
278281
}
279282

280283
// Tell the agent its name
@@ -289,7 +292,8 @@ We're going to have the agent ask us to name it, so we'll need a state property
289292

290293
return {
291294
...state,
292-
messages: response,
295+
// messages receives from the interrupt are not automatically saved to state, don't forget to add them!
296+
messages: [...state.messages, response],
293297
};
294298
}
295299
```
@@ -306,8 +310,8 @@ We're going to have the agent ask us to name it, so we'll need a state property
306310

307311
export function YourComponent() {
308312
useCopilotAction({
309-
name: "showCalendarMeeting",
310-
description: "Displays calendar meeting information",
313+
name: "AskName",
314+
description: "Ask the user how they would like to call you",
311315
parameters: [
312316
{
313317
name: "message",
@@ -346,6 +350,7 @@ We're going to have the agent ask us to name it, so we'll need a state property
346350
args={ "message": "Before we start, what would you like to call me?" } # The arguments to pass when the tool is called.
347351
)
348352
state["agent_name"] = answer
353+
state["messages"] = [*state["messages"], *messages]
349354

350355
# Tell the agent its name
351356
system_message = SystemMessage(
@@ -359,7 +364,8 @@ We're going to have the agent ask us to name it, so we'll need a state property
359364

360365
return {
361366
**state,
362-
"messages": response,
367+
# messages receives from the interrupt are not automatically saved to state, don't forget to add them!
368+
"messages": [*state["messages"], response],
363369
}
364370
```
365371
</Tab>
@@ -378,6 +384,7 @@ We're going to have the agent ask us to name it, so we'll need a state property
378384
args: { message: 'Before we start, what would you like to call me?' }, // The arguments to pass when the tool is called.
379385
}); // [!code highlight]
380386
state.agentName = answer
387+
state.messages = [...state.message, ...messages]
381388
}
382389

383390
// Tell the agent its name
@@ -392,7 +399,8 @@ We're going to have the agent ask us to name it, so we'll need a state property
392399

393400
return {
394401
...state,
395-
messages: response,
402+
// messages receives from the interrupt are not automatically saved to state, don't forget to add them!
403+
messages: [...state.messages, response],
396404
};
397405
}
398406
```
@@ -424,9 +432,14 @@ These can be used to notify the LLM about the recent communication:
424432
from copilotkit import copilotkit_interrupt
425433

426434
# ...
427-
agent_name, new_messages = copilotkit_interrupt(message="Before we start, what would you like to call me?")
428-
state["messages"] = state["messages"] + new_messages
429-
state["agent_name"] = agent_name
435+
async def chat_node(state: AgentState, config: RunnableConfig)
436+
agent_name, new_messages = copilotkit_interrupt(message="Before we start, what would you like to call me?")
437+
state["messages"] = state["messages"] + new_messages
438+
state["agent_name"] = agent_name
439+
# ... add the rest of the node implementation, including LLM calls etc.
440+
441+
# Don't forget to return the messages list with our newly added interrupt messages, and the new agent name
442+
return { "messages": state["messages"], "agent_name": state["agent_name"] }
430443
# ...
431444
```
432445
</Tab>
@@ -435,10 +448,15 @@ These can be used to notify the LLM about the recent communication:
435448
import { copilotKitInterrupt } from "@copilotkit/sdk-js/langgraph";
436449

437450
// ...
438-
const {agentName, newMessages} = copilotKitInterrupt("Before we start, what would you like to call me?");
439-
state.messages = [...state.messages, ...newMessages];
440-
state.agentName = agentName;
441-
// ...
451+
async function chat_node(state: AgentState, config: RunnableConfig) {
452+
const { agentName, messages } = copilotKitInterrupt({ message: "Before we start, what would you like to call me?" });
453+
state.messages = [...state.messages, ...messages];
454+
state.agentName = agentName;
455+
// ... add the rest of the node implementation, including LLM calls etc.
456+
457+
// Don't forget to return the messages list with our newly added interrupt messages, and the new agent name
458+
return { messages: state.messages, agentName: state.agentName }
459+
}
442460
```
443461
</Tab>
444462
</Tabs>

sdk-python/copilotkit/langgraph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,6 @@ def copilotkit_interrupt(
487487
"__copilotkit_interrupt_value__": interrupt_values,
488488
"__copilotkit_messages__": [interrupt_message]
489489
})
490-
answer = response.content
490+
answer = response[-1].content
491491

492-
return answer, [interrupt_message, response]
492+
return answer, response

sdk-python/copilotkit/langgraph_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ async def _stream_events( # pylint: disable=too-many-locals
280280

281281
# An active interrupt event that runs through messages. Use latest message as response
282282
if self.active_interrupt_event and interrupt_from_meta_events is None:
283-
resume_input = Command(resume=langchain_messages[-1])
283+
# state["messages"] only includes the messages we need to add at this point, tool call+result if applicable, and user text
284+
resume_input = Command(resume=state["messages"])
284285

285286
if interrupt_from_meta_events and "response" in interrupt_from_meta_events:
286287
resume_input = Command(resume=interrupt_from_meta_events["response"])

0 commit comments

Comments
 (0)