feat(agent): /interrupt slash command for in-flight agent turn #39
No reviewers
Labels
No labels
bug
commercial
documentation
duplicate
enhancement
feature
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jasoncouture/llama-shears!39
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/agent-interrupt-slash"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
/interruptcancels the agent's in-flight turn (model inference, eager tool dispatch) without affecting the agent's lifetime. Persisted context up to the interrupt is preserved; partial assistant text or thought fragments emitted by the canceled turn are dropped. The agent stays live and resumes on the next inbound message.Closes the Interrupt in-flight agent item under TASKS.md → "Web UI" (per the no-UI-buttons-for-control policy, surfaced as a slash command instead of a button).
What's in here
IAgent.InterruptAsync(Abstractions.Agent)New method on the public
IAgentcontract:Task InterruptAsync(CancellationToken). Idempotent.Agentimpl (Core)CancellationTokenSource _activeTurnCts, installed at the top of each batch inRunLoopAsync(linked to the run-loop's outer shutdown CT) and cleared on completion._interruptLockguards the field assignment against theInterruptAsynccaller.InterruptAsynccancels the per-turn CTS without affecting_shutdown. With no turn in flight,_activeTurnCtsis null and the call is a no-op.ProcessBatchAsync. The run-loop'scatch (OperationCanceledException) when (turnCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)path logs the interrupt and continues to the next batch.Plumbing
IAgentDirectory.InterruptAsync(string agentId, CancellationToken)— UI-side façade.AgentDirectory.InterruptAsync— looks up the agent on the manager and forwards.InterruptCommand : ISlashCommand—/interrupt, no parameters, calls into the directory. Registered inAddSlashCommands.Behavior at the interrupt point
InferenceRunnerstops publishing the moment it observes the cancellation, andPublishToolTurnsAsyncisn't reached, so no partialModelTurnis emitted on the bus → nothing lands in the persisted history.RunAsync(viaPublishToolTurnsAsync). Cancelling before that point leaves no orphantool_callin history. See "Out of scope" below.Tests
2 new TUnit cases (
AgentInterruptTests):InterruptOnIdleAgentIsNoOp— repeated calls without a turn in flight don't throw.InterruptCancelsInFlightTurnAndAgentRemainsLive— aHangingLanguageModelblocks forever on the cancellation token; afterInterruptAsync, the agent's processing gate releases and is reacquirable, proving the run-loop returned to idle.HangingLanguageModelis a small new fixture for tests that need to drive interrupt scenarios.Out of scope (deferred)
Persisting synthetic
"The user interrupted execution"tool-result entries for tool calls that were dispatched but didn't complete at interrupt time. The current pipeline persists tool turns in a single batch at end-of-RunAsync, so cancelling before that point leaves nothing on the bus to write. A follow-up can either (a) track in-flight tool dispatches at the agent level and synthesise pairedModelTurns on cancel, or (b) move tool-turn publishing into the per-call dispatch site.Test plan
dotnet test --solution LlamaShears.slnx— 391 passing (was 389).dotnet-test-on-source-changepre-commit check passed.docs-api-up-to-datepre-push check passed.Stacking note
Targets
feat/slash-command-registry(PR #38). Will retargetmainautomatically as the chain merges. This is the last PR in the 9-branch chain.🤖 Generated with Claude Code
Pull request overview
Adds an
/interruptslash command and corresponding agent API to cancel an in-flight agent turn (model inference / eager tool dispatch) while keeping the agent process alive for subsequent messages.Changes:
IAgentcontract withInterruptAsyncand implements per-turn cancellation inAgent’s run loop.IAgentDirectory+AgentDirectory) and a new/interruptISlashCommand.Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
InterruptAsyncAPI contract with documentation.InterruptAsync, and an “interrupted turn” log path in the run loop.InterruptAsyncon the UI-side agent directory abstraction./interruptslash command in DI./interruptcommand that invokes the directory interrupt endpoint.InterruptAsync.💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@ -0,0 +1,26 @@using System.Collections.Immutable;/interruptreturnsSlashCommandResult.Default, which means the dispatcher won’t perform any UI-side cleanup. If the agent is interrupted mid-stream, the UI can be left with a “streaming” bubble that never receives aFinal=truefragment (since inference stops abruptly). Consider adding a post-execute hint (e.g. “StopStreaming/RefreshView”) or otherwise clearing streaming state on interrupt so the chat view doesn’t get stuck.@ -125,6 +127,24 @@ public sealed partial class Agent : IAgent, IEventHandler<ChannelMessage>, IAsynreturn ValueTask.CompletedTask;}public Task InterruptAsync(CancellationToken cancellationToken)InterruptAsyncaccepts aCancellationTokenbut currently ignores it and always returns a completed task. If callers pass request-abort tokens (e.g. from the web UI), this method won’t respect cancellation semantics; either remove the parameter (if possible) or explicitly observe it (e.g., throw if already canceled) to match the rest of the API surface.@ -128,0 +139,4 @@CancellationTokenSource? cancellationTokenSource;lock (_interruptLock){cancellationTokenSource = _activeTurnCts;Calling
CancellationTokenSource.Cancel()while holding_interruptLockrisks running cancellation callbacks under the lock (callbacks can run synchronously), which can lead to lock inversion/deadlocks if any callback ever touches agent state that also uses_interruptLock. Safer pattern is to copy_activeTurnCtsto a local under the lock, release the lock, then callCancel()on the local reference.turnCts.Tokenis passed through toProcessBatchAsync, which means/interruptcan cancel before the user turn is published/persisted (the first await inProcessBatchAsyncuses this token). If an interrupt happens early in the turn, the inboundChannelMessagehas already been dequeued and the user message can be lost entirely. Consider using the per-turn CTS only for inference/tool dispatch, while publishing/persisting the user turn (and any other must-not-drop bookkeeping) under the outer run-loop/shutdown token instead.@ -42,4 +42,18 @@ public interface IAgent : IDisposable/// or unconfigured context is left alone.The XML docs say “partial assistant text or thought fragments … are dropped”, but fragment events (
AgentMessageFragment/AgentThoughtFragment) can already have been published before cancellation is observed, and there’s no corresponding “retract/clear” event. Consider clarifying that partial fragments are dropped from persisted history (ModelTurns), while live subscribers may have seen partial fragments.