Cancellation & resume
Cooperatively cancel in-flight invocations, and resume paused ones in place with checkpointed workflow agents.
Every invocation carries a cheap, cloneable CancellationToken; flipping it makes agents exit cleanly at the next safe point. Pair that with ResumabilityConfig and a paused invocation — waiting on a tool confirmation, an auth consent, or a long-running tool — can be continued in place with Runner::resume, keeping its invocation_id and skipping already-completed pipeline steps.
CancellationToken
The token wraps a single Arc<AtomicBool> — all clones share the same state, cancel() is idempotent, and checking costs one atomic load. The crate deliberately avoids tokio_util's token: agents observe the flag synchronously between awaits, so an AtomicBool suffices.
CancellationToken::new() -> Self- Fresh token in the "not cancelled" state.
CancellationToken::cancel(&self)- Flip to "cancelled". Idempotent; visible to every clone.
CancellationToken::is_cancelled(&self) -> bool- True once any clone has been cancelled.
InvocationContext::is_cancelled(&self) -> bool- Convenience check agents call at safe points — between iterations of the LLM↔tool loop and between sub-agents.
Cancellation is cooperative: an LlmAgent checks the flag at the top of each loop iteration, before issuing the next LLM call. A tool already in flight runs to completion, but no further turns are issued. The agent then emits a terminal event with error_code: "CANCELLED" and error_message: "invocation was cancelled", so consumers can distinguish a cancel from an organic stop.
Starting, observing, cancelling
Runner::run/run_with return just the event stream. When you need the id — to cancel from another task or HTTP handler — use Runner::start, which returns a RunningInvocation handle. The runner keeps an internal map of in-flight invocations; entries are registered in start and removed automatically when the stream ends, however it ends.
Runner::start(&self, user_id, session_id: Option<&str>, user_content: Content, run_config: RunConfig) -> Result<RunningInvocation>- Start an invocation and return its handle.
struct RunningInvocation { invocation_id: String, cancellation: CancellationToken, events: EventStream<'static> }- Handle for an in-flight invocation: the server-assigned id, a clone of the shared cancellation token, and the agent's event stream.
Runner::cancel(&self, invocation_id: &str) -> bool- Flip the matching invocation's token. Returns
falseif the id is unknown (finished or never started). Runner::is_active(&self, invocation_id: &str) -> bool- Whether an invocation with this id is currently registered as in-flight.
use adk_rs::core::RunConfig;
use adk_rs::genai_types::Content;
use futures::StreamExt;
let handle = runner
.start("user", None, Content::user_text("research this topic"), RunConfig::default())
.await?;
let invocation_id = handle.invocation_id.clone();
// Elsewhere (another task, an HTTP DELETE handler, ...):
assert!(runner.cancel(&invocation_id));
// Equivalent, with the handle in scope: handle.cancellation.cancel();
let mut events = handle.events;
while let Some(event) = events.next().await {
let event = event?;
if event.response.error_code.as_deref() == Some("CANCELLED") {
println!("stopped before the next LLM call");
}
}What pauses an invocation
Three gates make an agent stop issuing turns and hand control back to the caller, marking the invocation paused. In each case the pausing event carries the call id in long_running_tool_ids, and the caller resumes by resubmitting a FunctionResponse with that id:
- Tool confirmation — a
require_confirmationtool emitsadk_request_confirmationand waits for the human's decision. - Auth consent — a tool whose credential needs interactive OAuth2 consent emits
adk_request_credential. - Long-running tools — a tool marked
is_long_runningreturns a handle; the caller later resubmits the final result.
Resumability and checkpoints
By default a follow-up turn is a new invocation: a SequentialAgent pipeline re-runs from its first sub-agent (replay of the pending call is author-scoped, so earlier agents skip it safely). With resumability enabled, workflow agents instead record checkpoints as sub-agents complete — events whose actions.agent_state holds {"completed_sub_agents": n} — and Runner::resume continues the same invocation_id from the last checkpoint, never re-running finished steps.
RunnerBuilder::resumable(self, yes: bool) -> Self- App-level switch; sets
ResumabilityConfig { is_resumable }. Copied into each invocation'sRunConfigunless the caller setrun_config.resumabilityexplicitly. Runner::resume(&self, user_id: &str, session_id: &str, invocation_id: &str, new_content: Option<Content>, run_config: RunConfig) -> Result<RunningInvocation>- Resume a paused invocation in place.
new_contenttypically carries the unblockingFunctionResponse: a long-running result, anadk_request_confirmationdecision, or anadk_request_credentialconsent.
let runner = Runner::builder()
.app_name("ops")
.agent(pipeline) // SequentialAgent: [plan, deploy-with-confirmation]
.session_service(svc.clone())
.resumable(true)
.build()?;
let handle = runner
.start("user", None, Content::user_text("ship it"), RunConfig::default())
.await?;
let invocation_id = handle.invocation_id.clone();
handle.events.collect::<Vec<_>>().await; // pauses on the gate
// Later: resume the SAME invocation with the approval. Step one's
// checkpoint means it is not re-run.
let resumed = runner
.resume("user", &session_id, &invocation_id,
Some(approval_content), RunConfig::default())
.await?;
assert_eq!(resumed.invocation_id, invocation_id);- Tool confirmation — the HITL gate in detail.
- Authenticated tools — the consent pause and
adk_request_credential. - Runner — the full orchestration surface.
- Sessions & state — append-only sessions and persistence backends.