Class: LLM::Function::Window

Inherits:
Object
  • Object
show all
Defined in:
lib/llm/function/window.rb

Overview

The stretch of a call that an interrupt belongs to.

A raise cannot be aimed at a region of code, only permitted for one - it lands wherever the thread it targets happens to be. This is what permits it: an interrupt that arrives while the tool is running is raised on the tool at once, and one that arrives before the tool runs is held until it does, where the tool's own rescue can have it.

Held rather than answered, because arriving early is not the same as being declined: the tool has not had its chance yet. And afterwards there is nothing to interrupt - a cancel that arrives once the work is finished is a no-op, which is what Return#interrupt! already says one is.

What differs between the strategies is how the raise is issued, not where the edges are. A tool that runs on a thread is interrupted by raising on that thread; one that runs under a scheduler is interrupted by asking the scheduler, which schedules the raise and returns. Both are one call here, and everything else - the three states, and what "this call was interrupted" means - is this object's, so the strategies agree about where a call begins and ends.

A raise a call cannot take is not delivered outside the call. The scheduler places a scheduled raise, and a tool that never suspends gives it no suspension inside the call to be placed at - so one is left over. What is asserted is the half anything outside the call could notice, and it is the caller's frame rather than the tool's: the frame that waits on the task ends cleanly, and a sibling task on the same reactor runs.

Where the leftover goes is reasoned, not observed. The raise is aimed at the fibre the tool runs in, and the frame those examples watch is a different fibre - a raise in one is not the other's exception, so they cannot see it either way. That it is discarded rather than delivered follows from what the scheduler can place: a raise scheduled for a fibre is placed when that fibre resumes, and a fibre whose tool never suspends does not resume before it ends.

Instance Method Summary collapse

Constructor Details

#initialize(thread: nil, scheduler: nil, fiber: nil) ⇒ LLM::Function::Window

Parameters:

  • thread (Thread, nil) (defaults to: nil) —

    The thread the interrupt is raised on, which is the one running the tool. An argument rather than ::Thread.main so that a strategy whose tool runs elsewhere, and a spec, can say which thread they mean.

  • scheduler (Object, nil) (defaults to: nil) —

    A scheduler to ask instead of a thread to raise on - the Fiber.scheduler a tool that runs under one was given. Asking is not raising: fiber_interrupt schedules the raise and returns, so a canceller that is the tool's own thread is not left waiting on it. A scheduler that does not implement it - it is new enough that few do - is asked by raising on the fiber directly, which suspends the canceller until the scheduler delivers it, and which cannot hold a deferred ask.

  • fiber (Fiber, nil) (defaults to: nil) —

    The fiber the scheduler is asked to raise on, which is the one running the tool.



62
63
64
65
66
67
68
69
# File 'lib/llm/function/window.rb', line 62

def initialize(thread: nil, scheduler: nil, fiber: nil)
  @raise, @defer = interrupt_for(thread:, scheduler:, fiber:)
  @mutex = Mutex.new
  @changed = ConditionVariable.new
  @state = :idle
  @deferred = false
  @interrupted = false
end

Instance Method Details

#running? ⇒ Boolean

Whether the call is running.

Returns:

  • (Boolean)


137
138
139
# File 'lib/llm/function/window.rb', line 137

def running?
  @mutex.synchronize { @state == :running }
end

#interrupt!(wait: true)

This method returns an undefined value.

Asks the call to stop, waiting for it to start if this canceller can afford to.

The wait is a mutex and a condition variable, and whether it can be taken is the whole of what wait is for: the canceller is never the tool's thread on :thread, :fork and :ractor, so it waits, and it can be on a reactor - where a cancel can arrive on the thread the tool runs on - so there it does not.

A call that has not opened is not asked about, and the ask is not lost. With wait: false the ask is recorded and issued when the call opens, which is the transition #running! alerts - the only place that can, since the caller cannot wait and the call has not begun. Without that record the strategies disagree: :thread delivers an ask made in this moment, and a scheduled strategy drops it.

A deferred ask needs an asker that can hold one. The ask is issued at #running! only by an asker that schedules its raise; the fallback for a scheduler without fiber_interrupt raises on the fiber that asks, which at running! is the tool's own - so it would deliver before the call and skip the tool, which is the divergence these edges exist to close. It is refused instead, loudly, rather than quietly doing the thing this is here to prevent.

A call that is running is asked about at once, and one that has finished asks nothing. The raise is not issued before the call - the call's own dispatch is code, and a raise can land in that, the same gap Fork::Job has between running! and runner.call - and it is not issued for a call that has already finished.

Parameters:

  • wait (Boolean) (defaults to: true) —

    Whether to wait for the call to open.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/llm/function/window.rb', line 105

def interrupt!(wait: true)
  issue = @mutex.synchronize do
    @changed.wait(@mutex) while wait and @state == :idle
    if @state == :idle
      unless @defer
        raise LLM::FiberError,
          "this scheduler does not implement fiber_interrupt, so a cancel " \
          "cannot be held until the tool starts: it would be delivered " \
          "before the call, and the tool would never run"
      end
      @deferred = true
      false
    elsif @state == :running
      @interrupted = true
      true
    else
      false
    end
  end
  @raise.call if issue
end

#idle? ⇒ Boolean

Whether the call has not opened yet.

Returns:

  • (Boolean)


130
131
132
# File 'lib/llm/function/window.rb', line 130

def idle?
  @mutex.synchronize { @state == :idle }
end

#interrupted? ⇒ Boolean

Whether a raise has been issued for this call.

A caller that has to decide whether to tell a tool that it was cancelled asks this rather than reading a flag of its own: the window is the frame that decides whether a raise is the tool's to handle, so it is the frame that knows. It is true where a raise was issued, and false where none was - a call that has finished, and a cancel that arrived without one.

Issued, not delivered, and the two are not the same here. A strategy that asks a scheduler gets a raise that is scheduled rather than one that has landed - and a tool that never suspends is one it cannot land inside, so that tool is asked about and never reached. The flag is the notification that an ask was taken up, not a promise that the tool saw it, and a caller that needs the second has to get it from the tool.

Returns:

  • (Boolean)


159
160
161
# File 'lib/llm/function/window.rb', line 159

def interrupted?
  @mutex.synchronize { @interrupted }
end

#running!

This method returns an undefined value.

Called from the tool's thread, immediately before the call.

It does not wait for the watcher: it changes the state and returns, so the thread that is about to call the tool stays ahead of the thread that is about to interrupt it. By the time the watcher is scheduled, the tool is running.

An ask that could not wait is issued here, after the state has been changed and the lock released - so a caller that cannot wait is still answered, and answered once.



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/llm/function/window.rb', line 175

def running!
  issue = @mutex.synchronize do
    @state = :running
    @changed.broadcast
    if @deferred
      @deferred = false
      @interrupted = true
    else
      false
    end
  end
  @raise.call if issue
end

#finished!

This method returns an undefined value.

Called from the tool's thread, once it has returned - in the happy path before the result is written, and in ensure for every other. Wakes anyone holding an interrupt, who then finds there is nothing to interrupt.



195
196
197
198
199
200
# File 'lib/llm/function/window.rb', line 195

def finished!
  @mutex.synchronize do
    @state = :finished
    @changed.broadcast
  end
end