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, use the ai.defineTool() method and call ctx.interrupt():

import 'package:genkit/genkit.dart';
// Input schema for the interrupt tool
@Schema()
class QuestionInput {
final List<String> choices;
final bool? allowOther;
QuestionInput({required this.choices, this.allowOther});
}
final askQuestion = ai.defineTool(
name: 'askQuestion',
description: 'Use this to ask the user a clarifying question',
inputSchema: QuestionInput.$schema,
outputSchema: .string(), // The expected user answer type
fn: (input, ctx) {
// Trigger an interrupt with the input data as metadata
ctx.interrupt(input);
},
);

Note that the outputSchema of an interrupt tool corresponds to the response data you will provide (the user’s answer) as opposed to something that will be automatically populated by a tool function.

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:

final response = await ai.generate(
prompt: 'Ask me a movie trivia question.',
toolNames: ['askQuestion'],
);

Genkit immediately returns a response on receipt of an interrupt tool call.

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:

// Check if the generation stopped due to an interrupt
if (response.finishReason == FinishReason.interrupted) {
print('Generation interrupted.');
}
// Access the interrupt requests
if (response.interrupts.isNotEmpty) {
print('Found ${response.interrupts.length} interrupts');
}

Responding to an interrupt is done using the resume parameter on a subsequent generate call, making sure to pass in the existing message history.

Once resumed, the model re-enters the generation loop, including tool execution, until either it completes or another interrupt is triggered:

var response = await ai.generate(
prompt: 'Help me plan a backyard BBQ.',
config: GeminiOptions(
systemInstruction: 'Ask clarifying questions until you have a complete solution.',
),
toolNames: ['askQuestion'],
);
while (response.finishReason == FinishReason.interrupted) {
final resumeResponses = <InterruptResponse>[];
// Handle all interrupts (multiple can occur)
for (final part in response.interrupts) {
// In a real app, this would involve UI interaction
final input = part.toolRequest.input as QuestionInput;
final userAnswer = await askUser(input);
resumeResponses.add(InterruptResponse(part.toolRequestPart!, userAnswer));
}
// Resume generation
response = await ai.generate(
messages: response.messages, // Pass history
toolNames: ['askQuestion'],
interruptRespond: resumeResponses,
);
}
print(response.text);

Another common pattern for interrupts 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, check ctx.resumed to decide whether the action has been approved. It is null on the first call, so the tool interrupts. When the tool is restarted, it holds the metadata the client sent, which lets the transfer proceed:

ai.defineTool(
name: 'transfer_funds',
description: 'transfer funds, requires user approval',
inputSchema: ApprovalRequest.$schema,
fn: (input, ctx) async {
// `ctx.resumed` is null on the first call and carries the client's
// approval metadata when the tool is restarted.
final resumed = ctx.resumed;
final approved = resumed is Map && resumed['approved'] == true;
if (!approved) {
ctx.interrupt(input);
}
return 'Successfully transferred funds! Details: ${input.details}';
},
);

To restart the interrupted tool, build a restart part from the interrupted tool request with .restart(metadata) and pass it to the interruptRestart parameter on your next ai.generate call. The metadata surfaces to the tool as ctx.resumed, and the model re-executes the tool function:

if (response.finishReason == FinishReason.interrupted) {
final interrupt = response.interrupts.first;
// In your app logic: Ask the user to confirm the transfer...
// Once confirmed, restart the tool with approval metadata that the tool
// reads via `ctx.resumed`.
final response2 = await ai.generate(
// Ensure you pass the previous messages back
messages: response.messages,
toolNames: ['transfer_funds'],
// Restart the interrupted tool with approval metadata.
interruptRestart: [
interrupt.toolRequestPart!.restart({'approved': true}),
],
);
}