Class: Pikuri::Agent::Control::Cancellable
- Inherits:
-
Object
- Object
- Pikuri::Agent::Control::Cancellable
- Defined in:
- lib/pikuri/agent/control/cancellable.rb
Overview
Cooperative cancellation token. Built on the main thread and handed to
Pikuri::Agent#initialize via cancellable:; an out-of-band caller (a SIGINT
trap, a TUI key, an IPC handler) calls #cancel! to flip the flag, and
the next #check! on the run thread raises Cancelled — which
Pikuri::Agent#run_loop catches, emits as Event::Cancelled, and re-raises so
the REPL returns control to the user.
Cancellation boundary
The Agent calls #check! from before_tool_call — between an LLM
response requesting a tool and the tool invocation — the only point
where conversation state is consistent (no in-flight subprocess, no
half-applied write). An in-flight LLM HTTP call is not interrupted
(the response lands, then the next boundary trips), nor is an in-flight
tool (notably Bash). Both are the intentional "gentle cancel" v1 scope.
#sleep widens that boundary by one case: code waiting inside a tool
can wait through this instead of Kernel#sleep and become
interruptible. It reaches waits only — a thread blocked in IO (a Faraday
read, Subprocess#wait) is still uninterruptible, so most of a tool's
blocking is untouched by design.
Sub-agent semantics
Cancellation is a global "stop the whole tree" signal — the agent tool
shares the parent's Cancellable by reference with each child, so one
#cancel! stops the parent, every sub-agent, and the synthesizer. The
sharing rule lives at the spawn site, not here.
Thread-safe: #cancel! is meant to run on a thread other than the loop's
(a SIGINT trap while the agent runs on a worker). A plain boolean ivar
suffices under MRI — the only transition that matters is false → true
before the next #check!, and repeated #cancel! is idempotent.
Defined Under Namespace
Classes: Cancelled
Constant Summary collapse
- SLICE_SECONDS =
Longest a #sleep stays unresponsive to a #cancel!: it wakes on this cadence to re-check the flag. Polling rather than a
ConditionVariablebecause #cancel! must stay callable from a SIGINT trap, where taking aMutexraisesThreadError— so the waking side pays, and the signalling side stays a bare flag write. 0.1- NEVER =
A token that never trips — the null object for code that structurally needs one (a #sleep to wait through, a
cancellable:parameter) when the host wired no cancellation. Frozen, so a stray #cancel! raisesFrozenErrorrather than silently cancelling every agent that defaulted to it. Defined below #initialize so@cancelledis set. new.freeze
Instance Method Summary collapse
-
#cancel! ⇒ void
Flip the flag.
- #cancelled? ⇒ Boolean
-
#check! ⇒ void
Raise Cancelled when the flag is set, else no-op.
-
#initialize ⇒ Cancellable
constructor
A new instance of Cancellable.
-
#reset! ⇒ void
Reset the flag to armed.
- #sleep(seconds) ⇒ void
-
#to_s ⇒ String
Short label for Pikuri::Agent#to_s, reflecting the flag state.
Constructor Details
#initialize ⇒ Cancellable
Returns a new instance of Cancellable.
57 58 59 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 57 def initialize @cancelled = false end |
Instance Method Details
#cancel! ⇒ void
This method returns an undefined value.
Flip the flag. Safe off the loop thread and idempotent. Takes effect at the next #check! — see the class header for the gentle-cancel caveats.
75 76 77 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 75 def cancel! @cancelled = true end |
#cancelled? ⇒ Boolean
81 82 83 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 81 def cancelled? @cancelled end |
#check! ⇒ void
This method returns an undefined value.
Raise Cancelled when the flag is set, else no-op. Called from
Pikuri::Agent's before_tool_call wiring.
90 91 92 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 90 def check! raise Cancelled if @cancelled end |
#reset! ⇒ void
This method returns an undefined value.
Reset the flag to armed. Called by Pikuri::Agent at each turn start so a
stale cancel doesn't poison the next turn. Mid-loop
Interloper injections deliberately do not reset —
otherwise cancel-then-inject would lose the cancel (the injection
resets the flag before the next before_tool_call reads it).
128 129 130 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 128 def reset! @cancelled = false end |
#sleep(seconds) ⇒ void
This method returns an undefined value.
Wait seconds, cutting the wait short with Cancelled if #cancel!
lands while waiting — the drop-in for Kernel#sleep anywhere a tool
paces or polls:
cancellable.sleep(2.0) # returns after 2s, or raises on a cancel
Checks the flag before waiting at all, so an already-cancelled token
raises without a delay, and a non-positive seconds is still a
cancellation check rather than a no-op. Wakes every SLICE_SECONDS to
look, so a cancel is honoured within that.
109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 109 def sleep(seconds) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds loop do check! remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) return if remaining <= 0 # Kernel., or this recurses into itself. Kernel.sleep([remaining, SLICE_SECONDS].min) end end |
#to_s ⇒ String
Returns short label for Pikuri::Agent#to_s, reflecting the flag state.
133 134 135 |
# File 'lib/pikuri/agent/control/cancellable.rb', line 133 def to_s "Cancellable(#{@cancelled ? 'cancelled' : 'armed'})" end |