Skip to content

Pause generation using interrupts

Interrupts are a special kind of tool that can pause the LLM generation-and-tool-calling loop to return control back to you. When you’re ready, you can then resume generation by sending replies that the LLM processes for further generation.

The most common uses for interrupts fall into a few categories:

  • Human-in-the-Loop: Enabling the user of an interactive AI to clarify needed information or confirm the LLM’s action before it is completed, providing a measure of safety and confidence.
  • Async Processing: Starting an asynchronous task that can only be completed out-of-band, such as sending an approval notification to a human reviewer or kicking off a long-running background process.
  • Exit from an Autonomous Task: Providing the model a way to mark a task as complete, in a workflow that might iterate through a long series of tool calls.

All of the examples documented here assume that you have already set up a project with Genkit dependencies installed. If you want to run the code examples on this page, first complete the steps in the Get started guide.

Before diving too deeply, you should also be familiar with the following concepts:

At a high level, this is what an interrupt looks like when interacting with an LLM:

  1. The calling application prompts the LLM with a request. The prompt includes a list of tools, including at least one for an interrupt that the LLM can use to generate a response.
  2. The LLM generates either a complete response or a tool call request in a specific format. To the LLM, an interrupt call looks like any other tool call.
  3. If the LLM calls an interrupting tool, the Genkit library automatically pauses generation rather than immediately passing responses back to the model for additional processing.
  4. The developer checks whether an interrupt call is made, and performs whatever task is needed to collect the information needed for the interrupt response.
  5. The developer resumes generation by passing an interrupt response to the model. This action triggers a return to Step 2.

The most common kind of interrupt allows the LLM to request clarification from the user, for example by asking a multiple-choice question.

For this use case, register an interrupt tool with define_interrupt, or raise Interrupt from a normal tool when you need to pause conditionally.

from genkit import FinishReason, Genkit, respond_to_interrupt
from genkit_google_genai import GoogleAI
from pydantic import BaseModel, Field
ai = Genkit(
plugins=[GoogleAI()],
model='googleai/gemini-flash-latest',
)
class QuestionInput(BaseModel):
"""Input schema for the question tool."""
question: str = Field(description='the question to ask')
choices: list[str] = Field(description='the choices to display to the user')
allow_other: bool = Field(default=False, description='when true, allow write-ins')
ask_question = ai.define_interrupt(
name='ask_question',
description='Use this to ask the user a clarifying question.',
input_schema=QuestionInput,
)

The output you later provide with respond_to_interrupt is what the model sees as the tool result when generation resumes.

Interrupts are passed into the tools list when generating content, just like other types of tools. You can pass both normal tools and interrupts to the same generate call:

response = await ai.generate(
prompt='Ask me a movie trivia question.',
tools=[ask_question],
)

Genkit returns as soon as an interrupt tool is called (finish_reason=FinishReason.INTERRUPTED).

If you’ve passed one or more interrupts to your generate call, you need to check the response for interrupts so that you can handle them:

# You can check the finish_reason (use the enum for comparisons)
if response.finish_reason == FinishReason.INTERRUPTED:
print("Generation interrupted.")
# Or you can check if any interrupt requests are on the response
if response.interrupts:
print(f"Interrupts found: {len(response.interrupts)}")
for interrupt in response.interrupts:
tool_input = interrupt.tool_request.input
print(f"Question: {tool_input.get('question')}")
print(f"Choices: {tool_input.get('choices')}")

Resume with respond_to_interrupt and pass the result to resume_respond, keeping the existing message history:

# Get the user's answer (e.g., from user input)
user_answer = 'b' # User selected option b
interrupt = response.interrupts[0]
response = await ai.generate(
messages=response.messages,
resume_respond=respond_to_interrupt(user_answer, interrupt=interrupt),
tools=[ask_question],
)

For interactive applications, you’ll often need to handle multiple interrupts in a loop until the model completes its task:

async def interactive_session():
response = await ai.generate(
prompt='Help me plan a backyard BBQ.',
system='Ask clarifying questions until you have a complete solution.',
tools=[ask_question],
)
while response.interrupts:
answers = []
for interrupt in response.interrupts:
tool_input = interrupt.tool_request.input or {}
question = tool_input.get('question', 'Unknown question')
choices = tool_input.get('choices', [])
print(f"\nQuestion: {question}")
for i, choice in enumerate(choices):
print(f" {i + 1}. {choice}")
user_input = input("Your answer: ")
answers.append(respond_to_interrupt(user_input, interrupt=interrupt))
response = await ai.generate(
messages=response.messages,
resume_respond=answers,
tools=[ask_question],
)
print(f"\nFinal response: {response.text}")

You can also use interrupts within flows for more structured applications:

from genkit import Genkit
from genkit_google_genai import GoogleAI
from pydantic import BaseModel, Field
ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest')
class TriviaQuestion(BaseModel):
"""A trivia question with multiple choice answers."""
question: str = Field(description='the trivia question')
answers: list[str] = Field(description='multiple choice answers')
present_question = ai.define_interrupt(
name='present_question',
description='Presents a trivia question to the user.',
input_schema=TriviaQuestion,
)
@ai.flow()
async def play_trivia(theme: str) -> str:
"""Plays a trivia game on the given theme."""
response = await ai.generate(
prompt=f'Ask me a trivia question about {theme}.',
tools=[present_question],
)
if response.interrupts:
interrupt = response.interrupts[0]
question_data = interrupt.tool_request.input or {}
return (
f"Question: {question_data.get('question')}\n"
f"Answers: {question_data.get('answers')}"
)
return response.text

Another common pattern is the need to confirm an action that the LLM suggests before actually performing it. For example, a payments app might want the user to confirm certain kinds of transfers before proceeding.

When defining a tool, you can check your application state or use ctx.is_resumed() to determine whether the action has already been approved. If it’s the first execution, raise an Interrupt exception to pause the loop:

from genkit import Interrupt, ToolRunContext
from pydantic import BaseModel, Field
class TransferInput(BaseModel):
to_account: str
amount: float
@ai.tool()
async def transfer_money(input: TransferInput, ctx: ToolRunContext) -> dict:
"""Transfer money between accounts, with confirmation for large amounts."""
# Require confirmation for large transfers (only on first execution)
if not ctx.is_resumed() and input.amount > 100:
raise Interrupt({
'reason': 'confirm_large',
'to_account': input.to_account,
'amount': input.amount,
})
# Execute the transfer (runs when resumed after approval)
return {
'status': 'confirmed',
'message': f'Transferred ${input.amount} to {input.to_account}',
}

To restart the interrupted tool, use the restart_tool() function to construct a restarted tool request part, and pass it to the resume_restart parameter of ai.generate().

You can customize the restart behavior by providing optional arguments to restart_tool():

  • resumed_metadata: Pass arbitrary state (e.g. {'approved_by': 'user'}) to the tool context. The tool function can retrieve this via ctx.resumed_metadata.
  • replace_input: Provide a new input payload (e.g. a modified Pydantic model or dictionary) to re-run the tool with modified arguments.
from genkit import restart_tool, respond_to_interrupt
response = await ai.generate(
prompt='Transfer $250 to account ABC123',
tools=[transfer_money],
)
messages = response.messages
if response.interrupts:
interrupt = response.interrupts[0]
# Ask the user to confirm the transfer...
if user_approved:
# Rerun the tool by passing a restart part to resume_restart
restart = restart_tool(
interrupt=interrupt,
resumed_metadata={'approved_by': 'user'},
)
response = await ai.generate(
messages=messages,
resume_restart=restart,
tools=[transfer_money],
)
else:
# Decline by providing a direct response without re-running the tool
decline = respond_to_interrupt(
{'status': 'cancelled'},
interrupt=interrupt,
)
response = await ai.generate(
messages=messages,
resume_respond=decline,
tools=[transfer_money],
)
print(response.text)

Replacing input and accessing original input on restart

Section titled “Replacing input and accessing original input on restart”

If you decide to adjust the tool arguments upon restart (for example, asking the user to lower a transfer amount that exceeded limits), pass the adjusted input payload to replace_input:

# Inside your interrupt-handling loop:
meta = interrupt.tool_request.input
adjusted_input = TransferInput(to_account=meta.get('to_account'), amount=100.0)
restart = restart_tool(
interrupt=interrupt,
resumed_metadata={'approved_by': 'user'},
replace_input=adjusted_input,
)

When a tool is restarted with a replaced input, the original input arguments are automatically stashed. Inside the tool function, you can retrieve the original arguments by checking ctx.original_input (which will be a dictionary):

@ai.tool()
async def transfer_money(input: TransferInput, ctx: ToolRunContext) -> dict:
# ... interrupt logic ...
# Check if the input was replaced upon restart
if ctx.original_input:
original = ctx.original_input
print(f"Adjusted transfer amount from {original.get('amount')} to {input.amount}")
# Execute the transfer with the current (possibly adjusted) input
return {
'status': 'confirmed',
'message': f"Transferred ${input.amount} to {input.to_account}",
}