Cancellation

Ctrl+C interrupts a running task. The agent returns to Idle promptly, the in-flight LLM HTTP request is dropped, and any files already modified are listed.

How It Works

The CLI installs a tokio::signal::ctrl_c handler that fires a tokio_util::sync::CancellationToken. The agent holds a clone of the token and:

  • Checks token.is_cancelled() at every state transition and before every tool call.
  • Wraps the streaming LLM call in tokio::select! racing against token.cancelled(). When the token wins, the LLM future is dropped, which cancels the underlying reqwest request.

When cancellation lands, run_task returns Ok(TaskResult { cancelled: true, verified: false, summary, files_changed }):

  • summary — text from the most recent LLM turn, if any.
  • files_changed — every file the agent had time to mutate via files-write or files-edit.

Wiring Your Own Token

If you’re embedding merlin-core programmatically, register your own CancellationToken via the fluent setter:

use merlin_core::agent::Agent;
use tokio_util::sync::CancellationToken;

let token = CancellationToken::new();
let mut agent = Agent::new(provider, config, commands);
agent
    .on_event(|event| println!("{event:?}"))
    .with_cancellation(token.clone());

// Fire from anywhere -- UI button, parent supervisor, parent context.
token.cancel();

Tested

The integration test suite (crates/merlin-core/tests/integration_agent.rs) includes cancellation_returns_promptly_with_partial_result, which uses a HangingProvider that never produces a response. The test asserts that firing the token returns a cancelled = true result well within a 2-second timeout.