feat: an assistant run can be stopped mid-way #162
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/cancel-an-assistant-run"
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?
What's in
AssistantRunCompletedAction(cancelled: true)with no error, so no error banner appears, the reflection epic skips the turn, and the next message runs its tools normally again.AgentService.cancelRun()sets a per-run flag andbuildToolswraps every tool in a newCancellableTool, so from the next tool call on nothing executes: the tool answers a short "the user CANCELLED this run" text instead of touching the repository, the web or the approval gate. No individual tool file changed, so a future tool inherits the behaviour.ApprovalGate.close()/reopen()): every card still pending is denied, and a write tool that only reaches the gate after the cancel (e.g.entity_writeawaitinglistTags()first) gets an immediate refusal instead of raising a live card the user can no longer stop. The gate reopens when the next run wiresonRequest.Agenttakes no cancel token), so a cancel costs at least one more completion, and up tomaxToolRoundsof them if the model keeps calling tools; nothing app-side ends the loop, only the tools stop working.Verification
scripts/verify.sh appfrom the branch worktree at099f674, fresh: passed —All tests passed!(1131 app tests) followed by== verify OK (app) ==.scripts/verify.sh backendwas not run: no backend file is touched by this branch.assistant_panel_test.dart— Send only while idle, Stop only while running, oneCancelAssistantRunActionper press, button dead afterwards, Send back on a cancelled completion; bounded pumps only) and app state/agent layer (agent_cancel_test.dart, 11 tests — reducer, epics,CancellableTool,ApprovalGate, and full scripted runs against a fake model API and a fakeDoujinRepository). No REST, use-case or persistence layer was exercised, and nothing was run in the real app against a live server — the server side is reached only through fakes.git checkout -- <file>; every one went red:complete()'serror: _cancelled ? null : errormutated toerror: errorand, separately, toerror: null(both directions pinned by the new failing-completion test);cancelRun()reverted tocancelAll()instead ofclose();ApprovalGate.requestlosing its_closedshort-circuit;close()losing_closed = true;close()losingcancelAll();request()returningtrueinstead offalsewhile closed (red ontagUpdates, so the direction is pinned, not just the presence);_runTurnlosingapprovalGate.reopen();_mirrorEvent'sToolStartEventlosingif (_cancelled) break;;isCancelling => false; thecancelRunguard reduced to each half alone;_runTurn's finally losing_cancelled = false;;CancellableTool.executeand.parseParameterslosing their short-circuits;_reflectEpiclosing&& !a.cancelled;_cancelEpicunregistered; all four reducerisCancellinglines (Send, Clear, Completed, idle guard), each now reached by a fixture that starts fromisCancelling: true/ running rather than from the defaultAppState; the panel'sisCancelling ? null :made unconditional and itsif (idle)inverted. Regression sets alongside: agent_write_tools, agent_tools, assistant_panel, assistant_agent, entity_write_epics.Agentexposes no cancellation token (hence the one-more-completion limit), and the doc changes themselves (ADR 0024 amendment,docs/BACKLOG.md,docs/DEVELOPMENT.md, README wording).feat/the-assistant-can-write-chapters-and-variantsmust amend the same "Deliberately deferred" paragraph in ADR 0024 (variant/page/upload writes) and touches the same README assistant bullet. The hunks here are confined to the cancellation clause plus an appended amendment section; whoever merges second should re-read the amendment rather than resolving the hunk mechanically.Follow-ups
AgentandOpenRouterClient— an openrouter_dart PR plus a submodule re-pin; until then a cancel costs at least one more completion, and up tomaxToolRoundsif the model keeps calling tools.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Cancellation at the one boundary the app actually owns — the tool call — with the honest limit written down in the ADR instead of promised away in a tooltip. You even closed the approval gate so a write tool that was mid-read when Stop was pressed gets refused at the gate instead of raising a card nobody can stop anymore. That window — cancel lands between
entity_write's read and its ask — is exactly where a lazier design would have left a live card that executes a write after the cancel. I checked. It's closed~ ♡Verdict: ✅ Looks good to me~
I read the full diff and the full current contents of every changed file, cross-checked the vendored
Agent's tool-call sequence (ToolStartEvent→parseParameters→execute→ToolEndEvent, always paired, errors captured as results), and I could not find a single branch that produces wrong runtime behavior:CancellableTooldelegates all fourToolmembers plus the derivedtoApiJson— nothing leaks around the decorator, and wrapping at thebuildToolsseam means a future tool cannot be added unwrapped. That's the right abstraction, fufu~_cancelledis monotonic within a run, set only bycancelRun, cleared in the finally aftercomplete()reads it — the single-read trust inCancellableToolis earned, not assumed.complete(error: _cancelled ? null : error)covers every exit path (success, failure, thrown, finally-fallback) — a cancelled run can never raise the red banner, and the suppression is cancel-specific (an ordinary failure still reports; your own test pins both directions).ToolEndEvents update nothing (reducer no-ops on unknown ids), and a chip started before the cancel still completes.isCancellingarms (Send, Clear, Completed, idle guard) each have their own fixture starting from the stuck state — the belt-and-suspenders a stale flag deserves.And I don't take "mutation probes were run" on faith — I ran my own against the committed tree:
ApprovalGate.requestlosing its_closedshort-circuit → exactly the after-the-cancel gate test red;complete()losing the error suppression → the failing-completion test red; the finally losing_cancelled = false→ three next-run-leak tests red. All restored, tree pristine. Your tests have teeth~ ♪💡 Little ideas (non-blocking)~
'cancelled-${_history.length}'(agent_service.dart,cancelRun) — after heavy compaction shrinks the history, a later cancelled run could land on the same length and reuse the id. Nothing matches on assistant-message ids today so it's harmless, but a monotonic counter (like_captionCounter) would be future-proof if transcript entries ever gain update-by-id behavior. Same pattern exists in theassistant-/interim-siblings, so this is purely a "while you're in there someday" note.✅ What I liked~
maxToolRounds— instead of pretending an abort happened. Honest limits in documentation are rarer than they should be, fufu~onPosthook is lovely — cancelling at POST 2 is precisely "after the first round, before the second", and assertingrepo.tagPages == 1proves the second tool never touched the repository._cancelEpicmirrors_clearEpic's shape exactly; consistency with siblings makes the middleware readable at a glance.Automated review by Jibril · 2026-09-19
CI/CD: absent for head SHA
099f674(no coverage bot comment yet) · Local checks: flutter analyze 0 issues · agent_cancel + assistant_panel 20/20 · regression suites (agent_write_tools, agent_tools, assistant_agent, entity_write_epics) 106/106 · 3/3 independent mutation probes red