Class: LLM::Function::Fiber::Task
- Defined in:
- lib/llm/function/fiber/task.rb
Overview
LLM::Function::Fiber::Task wraps a function call in a scheduler-backed fiber for cooperative concurrent execution. The fiber is created lazily when #wait is called, not at construction time.
Requires Fiber.scheduler — without one, raise early in
#wait. Interrupting a running task raises
Interrupt on the fiber, which stops it at the next
yield point. A cancel that arrives before the fiber exists
is held rather than dropped, and spent inside the call: the
tool is entered, so a tool whose own rescue cleans up is
cleaned up by a cancel that arrived before it started, the
way it is on :thread, :fork and :ractor.
The scheduler must implement fiber_interrupt, which is how a raise
is asked for from the call's own fiber rather than issued before it.
Without it a cancel that arrived early could only be delivered before the
call, so the strategy refuses at #spawn - in the caller's hands, before
a fiber exists - rather than inside one, where the refusal would be an
unhandled exception and a caller waiting on a queue nothing fills.
A tool that implements on_interrupt is told on that fiber, once the
call has ended, rather than on the thread that cancelled it.
Instance Attribute Summary
Attributes inherited from Task
#finished_at, #function, #span, #started_at, #tracer
Instance Method Summary collapse
-
#initialize(fn, options = {}) ⇒ Task
constructor
A new instance of Task.
- #spawn ⇒ nil
- #alive? ⇒ Boolean
-
#interrupt! ⇒ nil
(also: #cancel!)
Tells the tool, and lets it answer.
- #wait ⇒ LLM::Function::Return, LLM::Interrupt (also: #value)
- #group_class ⇒ Class
Methods inherited from Task
Constructor Details
#initialize(fn, options = {}) ⇒ Task
Returns a new instance of Task.
32 33 34 |
# File 'lib/llm/function/fiber/task.rb', line 32 def initialize(fn, = {}) super end |
Instance Method Details
#spawn ⇒ nil
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/llm/function/fiber/task.rb', line 38 def spawn @span = @tracer&.on_tool_start( id: @function.id, name: @function.name, arguments: @function.arguments, model: @function.model ) return if intercepted? if Fiber.scheduler.nil? raise ArgumentError, "Fiber concurrency requires Fiber.scheduler" elsif !Fiber.scheduler.respond_to?(:fiber_interrupt) ## # Refused here rather than at the window, which is built inside the # fiber: a raise in there is an exception the fiber ends with, and # the caller waits on a queue that will never fill. 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" else ## # The body hands its ending back through a queue, the way # `LLM::Function::Async::Task` does, rather than through the fiber. # # **A fiber is not askable.** `Fiber.schedule` under `Async` returns # `Async::Scheduler#fiber`'s value, which is a plain fiber, and a # plain fiber has no `#value`; a fiber that has ended cannot be # resumed for one either. The result, and the exception a body ended # with, are therefore pushed where they can be taken from. # # The fiber also names itself, because the scheduler's return is not # promised: under `Async` it is nil for a block that has already # ended, and a block that raises on its first instruction - a held # cancel - is exactly that. `@fiber` is the return when there is one, # and that name when there is not. # # The scheduler is named here too, and not read in {#interrupt!}: # that runs on the canceller's thread, which is not the thread the # scheduler was installed on. @queue = Queue.new inner = nil fiber = Fiber.schedule do inner = Fiber.current @scheduler = Fiber.scheduler @window = LLM::Function::Window.new(scheduler: @scheduler, fiber: inner) ## # **A cancel that arrived before this fiber ran is held by the # window.** There is nothing to ask for yet, so the ask is # recorded and issued when the call opens - which is the # transition the window alerts, and the only place that can, since # this fiber is the one that will be running the tool. # # A canceller cannot wait for that here, which is the difference # from `:thread`: it may be the fiber running the tool, so a # canceller that waited would be waiting on the fiber it means to # interrupt. `wait: false` is the only shape this strategy can # take, and the deferred ask is what makes it work. @window.interrupt!(wait: false) if @cancelled begin ## # The call opens here, and it is what a deferred ask is issued # against. The tool is entered, which is what a held cancel used # to prevent - this fiber raised before the call was reached, so # the tool's own `rescue` never saw the interrupt. @window.running! @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) @queue << function.call rescue LLM::Interrupt => ex @queue << ex ensure @finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) ## # The tool has answered, so a cancel that arrives from here on # asks nothing - which is the no-op a cancel that arrives once # the call has returned is. @window.finished! ## # The hook runs on the fiber the call runs on, once the call has # ended, rather than on the thread that cancelled. See the note on # `LLM::Function::Thread::Task#spawn` for why it cannot run before # the call's frame has ended, and what the window's # `interrupted?` means. function.interrupt! if @window.interrupted? end end @fiber = fiber || inner nil end end |
#alive? ⇒ Boolean
130 131 132 |
# File 'lib/llm/function/fiber/task.rb', line 130 def alive? @fiber&.alive? || false end |
#interrupt! ⇒ nil Also known as: cancel!
Tells the tool, and lets it answer.
The ask is the window's, and it does not wait. This runs on the canceller's thread, which can be the thread the scheduler runs on, so a canceller that waited for the call to start would block the fiber it means to interrupt. A call that is running is asked about at once, one that has finished asks nothing, and one that has not opened holds the ask until it does.
Whether waiting would have been affordable is the window's to say, not
this task's: fiber_interrupt schedules the raise and returns, where
a direct raise on a fiber a scheduler owns suspends the caller until
the scheduler delivers it.
149 150 151 152 153 |
# File 'lib/llm/function/fiber/task.rb', line 149 def interrupt! @cancelled = true @window&.interrupt!(wait: false) nil end |
#wait ⇒ LLM::Function::Return, LLM::Interrupt Also known as: value
159 160 161 162 163 164 165 |
# File 'lib/llm/function/fiber/task.rb', line 159 def wait return @intercepted if intercepted? return @result.unbox if @result spawn unless @fiber @result = LLM::Object.from(unbox: @queue.pop) @result.unbox end |