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 false if 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.
Cancel from outside the streamrust
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_confirmation tool emits adk_request_confirmation and 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_running returns 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's RunConfig unless the caller set run_config.resumability explicitly.
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_content typically carries the unblocking FunctionResponse: a long-running result, an adk_request_confirmation decision, or an adk_request_credential consent.
Pause on a confirmation gate, resume in placerust
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);