Skip to content

Agent interrupts

Interrupts let a tool pause execution and return a tool request to the client. The client can approve, reject, provide missing data, refresh credentials, or ask the user a question, then resume the turn.

Use interrupts when the model can decide that outside input is needed but the tool should not proceed automatically. Common cases include human approval, missing user choices, risky operations, payments, external auth, and actions that need a fresh environment check.

In Genkit Dart, interrupts are modeled as tools that invoke ctx.interrupt() to pause execution and prompt the client for external validation or missing inputs.

import 'package:genkit/genkit.dart';
import 'package:schemantic/schemantic.dart';
part 'banking_agent.g.dart';
@Schema()
abstract class $UserApprovalInput {
@Field(description: 'The action to be approved')
String get action;
@Field(description: 'Details about the action')
String get details;
}
final userApproval = ai.defineTool(
name: 'userApproval',
description: 'Ask the user for approval before proceeding with a sensitive action.',
inputSchema: UserApprovalInput.$schema,
// No outputSchema needed: the output is provided by the client on resume
fn: (input, ctx) async => ctx.interrupt(),
);
final bankingAgent = ai.defineAgent(
name: 'bankingAgent',
system: 'If the user wants to transfer money, ALWAYS use userApproval.',
tools: [userApproval, transferMoney],
);

Interrupted tool requests surface on res.interrupts at turn end, or can be checked as tool requests inside stream chunks.

final res = await chat.send(text: 'Transfer $500 to savings.');
for (final interrupt in res.interrupts) {
print(interrupt.name);
print(interrupt.input);
}

When an interrupt occurs, the agent’s final turn finishReason resolves as interrupted.

Use respond() when the client has final tool outputs ready. This builder returns a ToolResponsePart that you pass to chat.resume() via its respond parameter. This resolves the tool call directly without re-executing the tool function on the server.

final res = await chat.send(text: 'Transfer $500 to savings.');
final approvals = res.interrupts.where((i) => i.name == 'userApproval').toList();
if (approvals.isNotEmpty) {
final continued = await chat.resume(
respond: [
approvals.first.respond({'approved': true, 'feedback': 'Looks good!'}),
],
);
print(continued.text);
}

Use restart() when the tool itself should execute again on the server after state, parameters, or external configurations are corrected by the user.

final res = await chat.send(text: 'Deploy code to production.');
final deployInterrupts = res.interrupts.where((i) => i.name == 'deployApproval').toList();
if (deployInterrupts.isNotEmpty) {
final continued = await chat.resume(
restart: [
deployInterrupts.first.restart(),
],
);
print(continued.text);
}

The restart builder preserves the original tool input and forces the server-side tool function to execute again with those parameters.

To prevent client-side spoofing, the runtime validates that every respond and restart entry matches an active paused tool call by name and unique reference ID.