Class: Pikuri::Agent::Control::Cancellable

Inherits:
Object
  • Object
show all
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 ConditionVariable because #cancel! must stay callable from a SIGINT trap, where taking a Mutex raises ThreadError — so the waking side pays, and the signalling side stays a bare flag write.

Returns:

  • (Float)

    seconds

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! raises FrozenError rather than silently cancelling every agent that defaulted to it. Defined below #initialize so @cancelled is set.

Returns:

new.freeze

Instance Method Summary collapse

Constructor Details

#initializeCancellable

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

Returns whether #cancel! has been called since the last #reset!; observable from any thread.

Returns:

  • (Boolean)

    whether #cancel! has been called since the last #reset!; observable from any thread.



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.

Raises:



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.

Parameters:

  • seconds (Float)

    how long to wait; <= 0 returns at once.

Raises:



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_sString

Returns short label for Pikuri::Agent#to_s, reflecting the flag state.

Returns:



133
134
135
# File 'lib/pikuri/agent/control/cancellable.rb', line 133

def to_s
  "Cancellable(#{@cancelled ? 'cancelled' : 'armed'})"
end