Class: LLM::Function
- Inherits:
-
Object
- Object
- LLM::Function
- Extended by:
- Registry
- Defined in:
- lib/llm/function.rb,
lib/llm/function/fork.rb,
lib/llm/function/task.rb,
lib/llm/function/array.rb,
lib/llm/function/group.rb,
lib/llm/function/ractor.rb,
lib/llm/function/window.rb,
lib/llm/function/fork/job.rb,
lib/llm/function/registry.rb,
lib/llm/function/fork/task.rb,
lib/llm/function/ractor/job.rb,
lib/llm/function/ractor/task.rb
Overview
The LLM::Function class represents a local function that can be called by an LLM. Most users should define tools as subclasses of Tool instead — Function is the lower-level building block that Tool wraps.
Defined Under Namespace
Modules: Array, Async, Fiber, Fork, Ractor, Registry, Sequential, Thread Classes: Group, Return, Task, Window
Instance Attribute Summary collapse
-
#id ⇒ String?
Returns the function ID.
-
#arguments ⇒ Hash, ...
Returns function arguments.
-
#tracer ⇒ LLM::Tracer?
Returns a tracer, or nil.
-
#model ⇒ String?
Returns a model name, or nil.
-
#intercept ⇒ Class<LLM::Intercept>?
Returns the intercept class that supervises this function, or nil.
Class Method Summary collapse
-
.interrupt(runner) ⇒ nil
private
Tells a runner that its call was interrupted, if it wants telling.
-
.isolated ⇒ Array<Symbol>
Returns strategies that execute in an isolated environment.
Instance Method Summary collapse
-
#runner ⇒ Object
Returns the bound function runner instance.
-
#==(other) ⇒ Boolean
(also: #eql?)
Compares functions by tool call ID when both sides have one.
-
#hash ⇒ Integer
Returns a hash value compatible with #==.
-
#initialize(name) {|self| ... } ⇒ Function
constructor
A new instance of Function.
-
#name(name = nil)
Set (or get) the function name.
-
#description(desc = nil)
Set (or get) the function description.
-
#params(p = nil) {|schema| ... } ⇒ LLM::Schema::Leaf?
Set (or get) the function parameters.
-
#define(klass = nil, &b)
(also: #def)
Set the function implementation.
-
#call ⇒ LLM::Function::Return
Call the function.
-
#task(strategy, options = {}) ⇒ LLM::Function::Task
Returns a function as a LLM::Function::Task.
-
#cancel(reason: "function call cancelled", **extra) ⇒ LLM::Function::Return
Returns a value that communicates that the function call was cancelled.
-
#interrupt! ⇒ nil
(also: #cancel!)
Notifies the function runner that the call was interrupted.
-
#called? ⇒ Boolean
Returns true when a function has been called.
-
#cancelled? ⇒ Boolean
Returns true when a function has been cancelled.
-
#skill? ⇒ Boolean
Returns true when this function is backed by a skill tool.
-
#pending? ⇒ Boolean
Returns true when a function has neither been called nor cancelled.
-
#unavailable ⇒ LLM::Function::Return
Returns an in-band error for an unresolved function call.
-
#budget_spent ⇒ LLM::Function::Return
Returns an in-band error that indicates the tool call budget has been spent.
- #mcp? ⇒ Boolean
- #a2a? ⇒ Boolean
- #return(value) ⇒ LLM::Function::Return
- #error(ex:, **extra) ⇒ LLM::Function::Return
- #adapt(provider) ⇒ Hash
Methods included from Registry
clear_registry!, find_by_name, lock, register, registry, registry_key, tool_name, unregister
Constructor Details
Instance Attribute Details
#id ⇒ String?
Returns the function ID
139 140 141 |
# File 'lib/llm/function.rb', line 139 def id @id end |
#arguments ⇒ Hash, ...
Returns function arguments
144 145 146 |
# File 'lib/llm/function.rb', line 144 def arguments @arguments end |
#tracer ⇒ LLM::Tracer?
Returns a tracer, or nil
176 177 178 |
# File 'lib/llm/function.rb', line 176 def tracer @tracer end |
#model ⇒ String?
Returns a model name, or nil
181 182 183 |
# File 'lib/llm/function.rb', line 181 def model @model end |
#intercept ⇒ Class<LLM::Intercept>?
Returns the intercept class that supervises this function, or nil. The context stamps the intercept onto the functions it binds, so any task built from this function checks it before the tool runs.
188 189 190 |
# File 'lib/llm/function.rb', line 188 def intercept @intercept end |
Class Method Details
.interrupt(runner) ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Tells a runner that its call was interrupted, if it wants telling.
Internal to the strategies that run the hook from their own side, where
the runner is a local rather than something #interrupt! can resolve:
the child's copy in :fork is not the parent's, and a ractor cannot be
handed the function at all. The argument is the runner and not the
function, which is what separates this from #interrupt!, and one lookup
means on_cancel keeps its precedence over on_interrupt everywhere.
73 74 75 76 77 78 |
# File 'lib/llm/function.rb', line 73 def self.interrupt(runner) return nil unless runner hook = %i[on_cancel on_interrupt].find { runner.respond_to?(_1) } runner.public_send(hook) if hook nil end |
.isolated ⇒ Array<Symbol>
Returns strategies that execute in an isolated environment.
57 58 59 |
# File 'lib/llm/function.rb', line 57 def self.isolated %i[fork ractor] end |
Instance Method Details
#runner ⇒ Object
Returns the bound function runner instance.
Resolved once and kept, so that the object an interrupt has to reach is the object that runs the call. A class-backed tool would otherwise build an instance here and discard it, leaving #interrupt! with a class to send the hook to.
An in-process strategy resolves it in #task, on the calling thread, before the task exists for another thread to interrupt. Every other path resolves it at the first call.
The tracer is assigned when the instance is resolved, so a tracer set after that does not reach it.
The instance belongs to this function object rather than to a call. The per-call copy is the one Message#functions makes, and that is what keeps the usual path to one call per instance; a second #task on one function, or a retry, reuses the instance and whatever its ivars hold.
497 498 499 500 501 502 503 |
# File 'lib/llm/function.rb', line 497 def runner @_runner ||= begin _runner = Class === @runner ? @runner.new : @runner _runner.tracer = @tracer if _runner.respond_to?(:tracer=) _runner end end |
#==(other) ⇒ Boolean Also known as: eql?
Compares functions by tool call ID when both sides have one.
158 159 160 161 162 163 |
# File 'lib/llm/function.rb', line 158 def ==(other) return true if equal?(other) return false unless self.class === other return false unless id && other.id id == other.id end |
#hash ⇒ Integer
Returns a hash value compatible with #==.
169 170 171 |
# File 'lib/llm/function.rb', line 169 def hash id ? id.hash : object_id.hash end |
#name(name = nil)
This method returns an undefined value.
Set (or get) the function name
205 206 207 208 209 210 211 |
# File 'lib/llm/function.rb', line 205 def name(name = nil) if name @name = name.to_s else @name end end |
#description(desc = nil)
This method returns an undefined value.
Set (or get) the function description
217 218 219 220 221 222 223 |
# File 'lib/llm/function.rb', line 217 def description(desc = nil) if desc @description = desc else @description end end |
#params(p = nil) {|schema| ... } ⇒ LLM::Schema::Leaf?
A parameter type that is given as a proc is resolved when the
parameters are rendered rather than here, so a tool can say
parameter :fruit, proc { Enum[Fruit.keys] } and ask the model
for what is available at the time of the call.
Set (or get) the function parameters
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/llm/function.rb', line 238 def params(p = nil) if block_given? params = yield(@schema) params = LLM::Schema.parse(params) if Hash === params if @params @params.merge!(params) else @params = params end elsif p params { p } else @params || LLM::Schema::Object.new({}) end end |
#define(klass = nil, &b) Also known as: def
This method returns an undefined value.
Set the function implementation
258 259 260 |
# File 'lib/llm/function.rb', line 258 def define(klass = nil, &b) @runner = klass || b end |
#call ⇒ LLM::Function::Return
Call the function
266 267 268 269 270 271 |
# File 'lib/llm/function.rb', line 266 def call llm = @tracer&.llm llm ? llm.with_tracer(@tracer) { call_function } : call_function ensure @called = true end |
#task(strategy, options = {}) ⇒ LLM::Function::Task
Returns a function as a LLM::Function::Task.
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/llm/function.rb', line 295 def task(strategy, = {}) = @intercept&. = .merge(intercepted: @intercept&.call(function: self, **)) = .merge(tracer:, intercepted: [:intercepted] || resolve(strategy)) case strategy when :sequential Sequential::Task.new(self, ) when :async LLM.require "async" unless defined?(::Async) Async::Task.new(self, ) when :thread Thread::Task.new(self, ) when :fiber Fiber::Task.new(self, ) when :fork LLM.require "xchan", "~> 0.24" unless defined?(::Chan::UNIXSocket) Fork::Task.new(self, ) when :ractor unless Class === @runner || LLM::Tool === @runner raise LLM::RactorError, "Ractor concurrency only supports class-based tools\n" \ "(function: #{@name}, runner: #{@runner.inspect})" end Ractor::Task.new(self, .merge(runner_class: @runner, id:, name:, arguments:, model:)) else raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :sequential, :thread, :fiber, :async, :fork, or :ractor" end end |
#cancel(reason: "function call cancelled", **extra) ⇒ LLM::Function::Return
Returns a value that communicates that the function call was cancelled
333 334 335 336 337 |
# File 'lib/llm/function.rb', line 333 def cancel(reason: "function call cancelled", **extra) Return.new(id, name, extra.merge(cancelled: true, reason:)) ensure @cancelled = true end |
#interrupt! ⇒ nil Also known as: cancel!
Notifies the function runner that the call was interrupted.
This is cooperative and only applies to runners that implement
on_cancel or on_interrupt.
Resolved through #runner rather than read off @runner, so that a
class-backed tool is told on the object the call will run on. Read off
the definition instead, a class answers respond_to? with false for
both hooks, and a tool that implements one is never told.
An in-process strategy has already resolved the runner in #task, on the thread that built the task, so this reads a memo rather than creating one. Where there is no tool to tell - one that cannot be built, or a function whose task was never made - this is a no-op rather than a raise, because the call path is where a constructor's failure belongs.
Six routes reach the hook, and they differ in where it runs and in whether an interrupt has to have been delivered first:
:thread,:fiberand:asyncrun it on the thread or fiber that ran the call, once the call has ended, and only where the raise was issued into a live job.:forkand:ractorrun it inside the child or the ractor, before the interrupt is delivered, and only for a message that arrived.:sequentialis the exception, and this method is how it is reached: nothing is raised into a sequential tool, so the hook is its only notification and it runs on the thread that cancelled, through LLM::Function::Sequential::Task#interrupt!.
A caller that reaches this directly - LLM::Context#interrupt! over
pending functions, say - takes the last route, with no delivery to speak
of and nothing running.
373 374 375 |
# File 'lib/llm/function.rb', line 373 def interrupt! LLM::Function.interrupt(runner_or_nil) end |
#called? ⇒ Boolean
Returns true when a function has been called
381 382 383 |
# File 'lib/llm/function.rb', line 381 def called? @called end |
#cancelled? ⇒ Boolean
Returns true when a function has been cancelled
388 389 390 |
# File 'lib/llm/function.rb', line 388 def cancelled? @cancelled end |
#skill? ⇒ Boolean
Returns true when this function is backed by a skill tool.
395 396 397 |
# File 'lib/llm/function.rb', line 395 def skill? @runner.respond_to?(:skill?) and @runner.skill? end |
#pending? ⇒ Boolean
Returns true when a function has neither been called nor cancelled
402 403 404 |
# File 'lib/llm/function.rb', line 402 def pending? !@called && !@cancelled end |
#unavailable ⇒ LLM::Function::Return
Returns an in-band error for an unresolved function call.
409 410 411 412 413 414 415 |
# File 'lib/llm/function.rb', line 409 def unavailable Return.new(id, name, { error: true, type: LLM::NoSuchToolError.name, message: "tool not found" }) end |
#budget_spent ⇒ LLM::Function::Return
Returns an in-band error that indicates the tool call budget has been spent.
421 422 423 424 425 426 427 |
# File 'lib/llm/function.rb', line 421 def budget_spent cancel( reason: "you are making too many tool calls", action: ["stop requesting tool calls"], advice: ["produce a response from tool calls already made", "temporary error that resets on the next turn"] ) end |
#mcp? ⇒ Boolean
431 432 433 |
# File 'lib/llm/function.rb', line 431 def mcp? Class === @runner and @runner.mcp? end |
#a2a? ⇒ Boolean
437 438 439 |
# File 'lib/llm/function.rb', line 437 def a2a? Class === @runner and @runner.a2a? end |
#return(value) ⇒ LLM::Function::Return
452 453 454 |
# File 'lib/llm/function.rb', line 452 define_method(:return) do |value| Return.new(id, name, value) end |
#error(ex:, **extra) ⇒ LLM::Function::Return
462 463 464 465 466 467 468 |
# File 'lib/llm/function.rb', line 462 def error(ex:, **extra) error = extra.merge({ error: true, type: ex.class.to_s, message: ex., backtrace: ex.backtrace }) self.return(error) end |
#adapt(provider) ⇒ Hash
472 473 474 |
# File 'lib/llm/function.rb', line 472 def adapt(provider) provider.adapt_function(self) end |