llm.rb 12.5.1 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +482 -0
  3. data/LICENSE +21 -93
  4. data/README.md +49 -159
  5. data/data/deepinfra.json +3 -0
  6. data/data/xai.json +1 -1
  7. data/lib/llm/a2a.rb +1 -1
  8. data/lib/llm/active_record/acts_as_agent.rb +32 -0
  9. data/lib/llm/active_record/acts_as_llm.rb +6 -6
  10. data/lib/llm/agent.rb +101 -26
  11. data/lib/llm/buffer.rb +85 -3
  12. data/lib/llm/compactor/null.rb +19 -0
  13. data/lib/llm/compactor/truncate.rb +80 -0
  14. data/lib/llm/compactor.rb +42 -124
  15. data/lib/llm/context.rb +33 -37
  16. data/lib/llm/contract.rb +4 -25
  17. data/lib/llm/function/array.rb +15 -14
  18. data/lib/llm/function/async/group.rb +54 -0
  19. data/lib/llm/function/async/reactor.rb +48 -0
  20. data/lib/llm/function/async/task.rb +83 -0
  21. data/lib/llm/function/fiber/group.rb +46 -0
  22. data/lib/llm/function/fiber/task.rb +62 -0
  23. data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
  24. data/lib/llm/function/fork/job.rb +4 -3
  25. data/lib/llm/function/fork/task.rb +20 -10
  26. data/lib/llm/function/group.rb +40 -0
  27. data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
  28. data/lib/llm/function/ractor/job.rb +19 -3
  29. data/lib/llm/function/ractor/mailbox.rb +9 -0
  30. data/lib/llm/function/ractor/task.rb +24 -15
  31. data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
  32. data/lib/llm/function/sequential/task.rb +49 -0
  33. data/lib/llm/function/task.rb +25 -37
  34. data/lib/llm/function/thread/group.rb +46 -0
  35. data/lib/llm/function/thread/task.rb +60 -0
  36. data/lib/llm/function/tracing.rb +2 -0
  37. data/lib/llm/function.rb +56 -64
  38. data/lib/llm/loop_guard.rb +1 -2
  39. data/lib/llm/mcp.rb +22 -0
  40. data/lib/llm/object.rb +2 -1
  41. data/lib/llm/provider.rb +6 -3
  42. data/lib/llm/providers/google.rb +2 -2
  43. data/lib/llm/repl/command.rb +35 -8
  44. data/lib/llm/repl/commands/compact.rb +33 -0
  45. data/lib/llm/repl/input.rb +80 -15
  46. data/lib/llm/repl/markdown/table.rb +76 -0
  47. data/lib/llm/repl/markdown.rb +31 -1
  48. data/lib/llm/repl/status.rb +1 -1
  49. data/lib/llm/repl/stream.rb +10 -3
  50. data/lib/llm/repl/transcript.rb +1 -1
  51. data/lib/llm/repl/walker.rb +46 -0
  52. data/lib/llm/repl.rb +18 -12
  53. data/lib/llm/response.rb +10 -0
  54. data/lib/llm/schema/leaf.rb +5 -0
  55. data/lib/llm/schema/object.rb +11 -5
  56. data/lib/llm/sequel/agent.rb +32 -0
  57. data/lib/llm/sequel/plugin.rb +6 -6
  58. data/lib/llm/stream.rb +24 -17
  59. data/lib/llm/tool/param.rb +12 -0
  60. data/lib/llm/tool.rb +20 -4
  61. data/lib/llm/tools/chdir.rb +0 -2
  62. data/lib/llm/tools/git.rb +8 -4
  63. data/lib/llm/tools/mkdir.rb +1 -1
  64. data/lib/llm/tools/pwd.rb +0 -2
  65. data/lib/llm/tools/read_file.rb +0 -2
  66. data/lib/llm/tools/rg.rb +8 -4
  67. data/lib/llm/tools/shell.rb +8 -4
  68. data/lib/llm/tools/utils.rb +31 -0
  69. data/lib/llm/version.rb +1 -1
  70. data/lib/llm.rb +25 -5
  71. data/llm.gemspec +3 -3
  72. data/resources/deepdive.md +693 -57
  73. metadata +24 -13
  74. data/lib/llm/function/call_task.rb +0 -46
  75. data/lib/llm/function/fiber_group.rb +0 -105
  76. data/lib/llm/function/task_group.rb +0 -97
  77. data/lib/llm/function/thread_group.rb +0 -102
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Function::Async
4
+ ##
5
+ # Wraps an array of {Async::Task} objects running on a shared
6
+ # {LLM::Function::Async::Reactor}. The reactor is created on
7
+ # demand if not provided.
8
+ class Group < LLM::Function::Group
9
+ ##
10
+ # @param [Array<Async::Task>] tasks
11
+ # @param [Hash] options
12
+ # @option options [LLM::Function::Async::Reactor] :reactor
13
+ def initialize(tasks, options = {})
14
+ @tasks = tasks
15
+ @reactor = options[:reactor] || LLM::Function::Async::Reactor.new
16
+ end
17
+
18
+ ##
19
+ # @return [nil]
20
+ def spawn
21
+ @tasks.each do |task|
22
+ task.reactor = @reactor
23
+ task.spawn
24
+ end
25
+ nil
26
+ ensure
27
+ @spawned = true
28
+ end
29
+
30
+ ##
31
+ # @return [Boolean]
32
+ def alive?
33
+ @tasks.any?(&:alive?)
34
+ end
35
+
36
+ ##
37
+ # @return [nil]
38
+ def interrupt!
39
+ @tasks.each(&:interrupt!)
40
+ nil
41
+ end
42
+ alias_method :cancel!, :interrupt!
43
+
44
+ ##
45
+ # @return [Array<LLM::Function::Return>]
46
+ def wait
47
+ spawn unless @spawned
48
+ @tasks.map(&:wait)
49
+ ensure
50
+ @reactor.stop
51
+ end
52
+ alias_method :value, :wait
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Function::Async
4
+ ##
5
+ # Manages an {::Async::Reactor} on a background thread. Work
6
+ # is submitted through a thread-safe queue and run inside the
7
+ # reactor. The reactor and its fibers stay on one thread.
8
+ class Reactor
9
+ ##
10
+ # @return [Thread]
11
+ attr_reader :thread
12
+
13
+ def initialize
14
+ @inbox = Queue.new
15
+ @thread = ::Thread.new { run }
16
+ end
17
+
18
+ ##
19
+ # Submit a block to run inside the reactor.
20
+ # @return [nil]
21
+ def submit(&block)
22
+ @inbox << block
23
+ nil
24
+ end
25
+
26
+ ##
27
+ # Stop the reactor and wait for the thread to finish.
28
+ def stop
29
+ @inbox << :stop
30
+ @thread.join(5)
31
+ @thread.kill if @thread.alive?
32
+ end
33
+
34
+ private
35
+
36
+ def run
37
+ reactor = ::Async::Reactor.new
38
+ reactor.async do
39
+ loop do
40
+ work = @inbox.pop
41
+ break if work == :stop
42
+ reactor.async { work.call }
43
+ end
44
+ end
45
+ reactor.run
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Function::Async
4
+ ##
5
+ # {LLM::Function::Async::Task} wraps a function call in an
6
+ # {Async::Task} running on a shared
7
+ # {LLM::Function::Async::Reactor}. The task is spawned lazily
8
+ # in {#wait} or explicitly in {#spawn}.
9
+ #
10
+ # Work is submitted to the reactor through its inbox queue.
11
+ # Results are bridged back through the task's own queue.
12
+ class Task < LLM::Function::Task
13
+ ##
14
+ # @param [LLM::Function] fn
15
+ # @param [Hash] options
16
+ # @option options [LLM::Function::Async::Reactor] :reactor
17
+ def initialize(fn, options = {})
18
+ super
19
+ @reactor = options[:reactor]
20
+ end
21
+
22
+ ##
23
+ # Assign the reactor. Used when a task is created before its
24
+ # reactor is available.
25
+ # @param [LLM::Function::Async::Reactor] reactor
26
+ def reactor=(reactor)
27
+ @reactor = reactor
28
+ end
29
+
30
+ ##
31
+ # Submit the function call to the reactor. The result is
32
+ # pushed to a queue that {#wait} consumes.
33
+ # @return [nil]
34
+ def spawn
35
+ @queue = Queue.new
36
+ @alive = true
37
+ @reactor.submit do
38
+ @queue << function.call
39
+ rescue LLM::Interrupt => e
40
+ @queue << e
41
+ raise
42
+ end
43
+ nil
44
+ end
45
+
46
+ ##
47
+ # @return [Boolean]
48
+ def alive?
49
+ @alive || false
50
+ end
51
+
52
+ ##
53
+ # Push an interrupt sentinel to the result queue. The reactor
54
+ # thread continues running but the result is discarded.
55
+ # @return [nil]
56
+ def interrupt!
57
+ if @queue
58
+ @alive = false
59
+ @queue << LLM::Interrupt.new
60
+ end
61
+ nil
62
+ end
63
+ alias_method :cancel!, :interrupt!
64
+
65
+ ##
66
+ # Wait for the result queue to contain a value.
67
+ # @return [LLM::Function::Return]
68
+ def wait
69
+ spawn unless @queue
70
+ result = @queue.pop
71
+ @alive = false
72
+ raise result if LLM::Interrupt === result
73
+ result
74
+ end
75
+ alias_method :value, :wait
76
+
77
+ ##
78
+ # @return [Class]
79
+ def group_class
80
+ LLM::Function::Async::Group
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Function::Fiber
4
+ ##
5
+ # Wraps an array of {Fiber::Task} objects for cooperative
6
+ # concurrent execution. Interrupts all tasks and waits
7
+ # for them to complete.
8
+ class Group < LLM::Function::Group
9
+ ##
10
+ # @param [Array<LLM::Function::Fiber::Task>] tasks
11
+ def initialize(tasks)
12
+ @tasks = tasks
13
+ end
14
+
15
+ ##
16
+ # @return [nil]
17
+ def spawn
18
+ @tasks.each(&:spawn)
19
+ nil
20
+ ensure
21
+ @spawned = true
22
+ end
23
+
24
+ ##
25
+ # @return [Boolean]
26
+ def alive?
27
+ @tasks.any?(&:alive?)
28
+ end
29
+
30
+ ##
31
+ # @return [nil]
32
+ def interrupt!
33
+ @tasks.each(&:interrupt!)
34
+ nil
35
+ end
36
+ alias_method :cancel!, :interrupt!
37
+
38
+ ##
39
+ # @return [Array<LLM::Function::Return>]
40
+ def wait
41
+ spawn unless @spawned
42
+ @tasks.map(&:wait)
43
+ end
44
+ alias_method :value, :wait
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Function::Fiber
4
+ ##
5
+ # {LLM::Function::Fiber::Task LLM::Function::Fiber::Task}
6
+ # wraps a function call in a scheduler-backed fiber for
7
+ # cooperative concurrent execution. The fiber is created
8
+ # lazily when {#wait} is called, not at construction time.
9
+ #
10
+ # Requires `Fiber.scheduler` — without one, raise early in
11
+ # {#wait}. Interrupting a running task raises
12
+ # {LLM::Interrupt} on the fiber, which stops it at the next
13
+ # yield point.
14
+ class Task < LLM::Function::Task
15
+ ##
16
+ # @param [LLM::Function] fn
17
+ # @param [Hash] options
18
+ def initialize(fn, options = {})
19
+ super
20
+ end
21
+
22
+ ##
23
+ # @return [nil]
24
+ def spawn
25
+ if Fiber.scheduler.nil?
26
+ raise ArgumentError, "Fiber concurrency requires Fiber.scheduler"
27
+ else
28
+ @fiber = Fiber.schedule { function.call }
29
+ nil
30
+ end
31
+ end
32
+
33
+ ##
34
+ # @return [Boolean]
35
+ def alive?
36
+ @fiber&.alive? || false
37
+ end
38
+
39
+ ##
40
+ # @return [nil]
41
+ def interrupt!
42
+ @fiber&.raise(LLM::Interrupt) if @fiber&.alive?
43
+ function.interrupt!
44
+ nil
45
+ end
46
+ alias_method :cancel!, :interrupt!
47
+
48
+ ##
49
+ # @return [LLM::Function::Return]
50
+ def wait
51
+ spawn unless @fiber
52
+ @result ||= @fiber.value
53
+ end
54
+ alias_method :value, :wait
55
+
56
+ ##
57
+ # @return [Class]
58
+ def group_class
59
+ LLM::Function::Fiber::Group
60
+ end
61
+ end
62
+ end
@@ -1,17 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Function
3
+ module LLM::Function::Fork
4
4
  ##
5
- # The {LLM::Function::Fork::Group} class wraps an array of
6
- # {LLM::Function::Task} objects that are running in forked child processes.
7
- class Fork::Group
5
+ # Wraps an array of {Fork::Task} objects that are running
6
+ # in forked child processes.
7
+ class Group < LLM::Function::Group
8
8
  ##
9
9
  # @param [Array<LLM::Function::Task>] tasks
10
- # @return [LLM::Function::Fork::Group]
11
10
  def initialize(tasks)
12
11
  @tasks = tasks
13
12
  end
14
13
 
14
+ ##
15
+ # @return [nil]
16
+ def spawn
17
+ @tasks.each(&:spawn)
18
+ nil
19
+ ensure
20
+ @spawned = true
21
+ end
22
+
15
23
  ##
16
24
  # @return [Boolean]
17
25
  def alive?
@@ -29,6 +37,7 @@ class LLM::Function
29
37
  ##
30
38
  # @return [Array<LLM::Function::Return>]
31
39
  def wait
40
+ spawn unless @spawned
32
41
  @tasks.map(&:wait)
33
42
  end
34
43
  alias_method :value, :wait
@@ -24,6 +24,8 @@ class LLM::Function
24
24
  runner = @function.runner
25
25
  controller = setup(runner)
26
26
  @ch.result.write([:result, call!(runner)])
27
+ rescue LLM::Interrupt
28
+ @ch.result.write([:interrupt])
27
29
  rescue => ex
28
30
  @ch.result.write([:result, error(ex)])
29
31
  ensure
@@ -52,12 +54,11 @@ class LLM::Function
52
54
 
53
55
  def setup(runner)
54
56
  ready = Queue.new
55
- thread = Thread.new do
57
+ thread = ::Thread.new do
56
58
  ready << true
57
59
  kind = @ch.control.recv
58
60
  next unless kind == :interrupt
59
- hook = %i[on_cancel on_interrupt].find { runner.respond_to?(_1) }
60
- runner.public_send(hook) if hook
61
+ ::Thread.main.raise(LLM::Interrupt)
61
62
  rescue IOError, ArgumentError
62
63
  end
63
64
  ready.pop
@@ -4,31 +4,39 @@ class LLM::Function
4
4
  ##
5
5
  # The {LLM::Function::Fork::Task} class wraps a fork-backed function call
6
6
  # and exchanges control and result messages with the child process.
7
- class Fork::Task
7
+ class Fork::Task < LLM::Function::Task
8
8
  ##
9
- # @param [LLM::Function] function
10
- # @param [LLM::Tracer, nil] tracer
11
- # @param [Object, nil] span
9
+ # @param [LLM::Function] fn
10
+ # @param [Hash] options
11
+ # @option options [LLM::Tracer, nil] :tracer
12
12
  # @return [LLM::Function::Fork::Task]
13
- def initialize(function, tracer: nil, span: nil)
14
- @function = function
15
- @tracer = tracer
16
- @span = span
13
+ def initialize(fn, options = {})
14
+ super
15
+ @tracer = options.fetch(:tracer, nil)
16
+ @spawned = false
17
17
  @waited = false
18
18
  end
19
19
 
20
20
  ##
21
21
  # @return [LLM::Function::Fork::Task]
22
22
  def spawn
23
- @ch = LLM::Object.from(control: xchan(:marshal), result: xchan(:marshal))
23
+ @span = @tracer&.on_tool_start(
24
+ id: @function.id, name: @function.name,
25
+ arguments: @function.arguments, model: @function.model
26
+ )
27
+ @ch = LLM::Object.from(
28
+ control: xchan(:marshal),
29
+ result: xchan(:marshal, sock: Socket::SOCK_STREAM)
30
+ )
24
31
  @pid = Kernel.fork { Fork::Job.new(@function, @ch).call }
32
+ @spawned = true
25
33
  self
26
34
  end
27
35
 
28
36
  ##
29
37
  # @return [Boolean]
30
38
  def alive?
31
- return false if @waited
39
+ return false if @waited || !@pid
32
40
  result = ::Process.waitpid(@pid, ::Process::WNOHANG)
33
41
  @waited = !result.nil?
34
42
  !@waited
@@ -51,7 +59,9 @@ class LLM::Function
51
59
  ##
52
60
  # @return [LLM::Function::Return]
53
61
  def wait
62
+ spawn unless @spawned
54
63
  kind, data = @ch.result.recv
64
+ raise LLM::Interrupt if kind == :interrupt
55
65
  raise ArgumentError, "Unknown fork message: #{kind.inspect}" unless kind == :result
56
66
  result = Return.new(data[:id], data[:name], data[:value])
57
67
  reap
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
4
+ ##
5
+ # Abstract base class for all concurrency strategy groups.
6
+ # Each strategy (sequential, thread, fiber, async, fork, ractor)
7
+ # subclasses this and implements {#alive?}, {#interrupt!}, and
8
+ # {#wait}.
9
+ class Group
10
+ ##
11
+ # @abstract
12
+ # @return [nil]
13
+ def spawn
14
+ raise NotImplementedError
15
+ end
16
+
17
+ ##
18
+ # @abstract
19
+ # @return [Boolean]
20
+ def alive?
21
+ raise NotImplementedError
22
+ end
23
+
24
+ ##
25
+ # @abstract
26
+ # @return [nil]
27
+ def interrupt!
28
+ raise NotImplementedError
29
+ end
30
+ alias_method :cancel!, :interrupt!
31
+
32
+ ##
33
+ # @abstract
34
+ # @return [Array<LLM::Function::Return>]
35
+ def wait
36
+ raise NotImplementedError
37
+ end
38
+ alias_method :value, :wait
39
+ end
40
+ end
@@ -1,18 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Function
3
+ module LLM::Function::Ractor
4
4
  ##
5
- # The {LLM::Function::Ractor::Group} class wraps an array of
6
- # {LLM::Function::Ractor::Task} objects that are running
5
+ # Wraps an array of {Ractor::Task} objects that are running
7
6
  # {LLM::Function} calls concurrently.
8
- class Ractor::Group
7
+ class Group < LLM::Function::Group
9
8
  ##
10
9
  # @param [Array<LLM::Function::Task>] tasks
11
- # @return [LLM::Function::Ractor::Group]
12
10
  def initialize(tasks)
13
11
  @tasks = tasks
14
12
  end
15
13
 
14
+ ##
15
+ # @return [nil]
16
+ def spawn
17
+ @tasks.each(&:spawn)
18
+ nil
19
+ ensure
20
+ @spawned = true
21
+ end
22
+
16
23
  ##
17
24
  # @return [Boolean]
18
25
  def alive?
@@ -30,6 +37,7 @@ class LLM::Function
30
37
  ##
31
38
  # @return [Array<LLM::Function::Return>]
32
39
  def wait
40
+ spawn unless @spawned
33
41
  @tasks.map(&:wait)
34
42
  end
35
43
  alias_method :value, :wait
@@ -35,22 +35,38 @@ class LLM::Function
35
35
  waiters = []
36
36
  loop do
37
37
  case ::Ractor.receive
38
- in [:done, *result]
38
+ in [:done, *data]
39
+ result ||= data
39
40
  done = true
40
41
  waiters.each { _1.send(result) }
42
+ break unless waiters.empty?
41
43
  waiters.clear
42
44
  in [:alive?, reply]
43
45
  reply.send(!done)
44
46
  in [:wait, reply]
45
- done ? reply.send(result) : waiters << reply
47
+ if done
48
+ reply.send(result)
49
+ break
50
+ else
51
+ waiters << reply
52
+ end
53
+ in [:interrupt]
54
+ @tool&.send(:interrupt)
46
55
  end
47
56
  end
48
57
  end
49
58
 
50
59
  def spawn
51
- ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
60
+ @tool = ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
61
+ ::Thread.new do
62
+ ::Ractor.receive == :interrupt or next
63
+ ::Thread.main.raise(LLM::Interrupt)
64
+ rescue ::Ractor::Error
65
+ end
52
66
  kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
53
67
  mailbox.send([:done, id, name, runner_class.new.call(**kwargs)])
68
+ rescue LLM::Interrupt
69
+ mailbox.send([:done, id, name, {cancelled: true, reason: "interrupted"}])
54
70
  rescue => ex
55
71
  mailbox.send([:done, id, name, {error: true, type: ex.class.name, message: ex.message}])
56
72
  end
@@ -20,6 +20,8 @@ class LLM::Function
20
20
  # @return [Boolean]
21
21
  def alive?
22
22
  request(:alive?)
23
+ rescue ::Ractor::ClosedError
24
+ false
23
25
  end
24
26
 
25
27
  ##
@@ -28,6 +30,13 @@ class LLM::Function
28
30
  request(:wait)
29
31
  end
30
32
 
33
+ ##
34
+ # @return [nil]
35
+ def interrupt!
36
+ task.send([:interrupt])
37
+ nil
38
+ end
39
+
31
40
  private
32
41
 
33
42
  def request(type)
@@ -5,31 +5,38 @@ class LLM::Function
5
5
  # The {LLM::Function::Ractor::Task} class wraps a ractor-backed function
6
6
  # call and delegates mailbox coordination to
7
7
  # {LLM::Function::Ractor::Mailbox}.
8
- class Ractor::Task
8
+ class Ractor::Task < LLM::Function::Task
9
9
  ##
10
10
  # @return [LLM::Function::Ractor::Mailbox]
11
11
  attr_reader :mailbox
12
12
 
13
13
  ##
14
- # @param [Class] runner_class
15
- # @param [String, nil] id
16
- # @param [String] name
17
- # @param [Hash, Array, nil] arguments
18
- # @param [LLM::Tracer, nil] tracer
19
- # @param [Object, nil] span
14
+ # @param [LLM::Function] fn
15
+ # @param [Hash] options
16
+ # @option options [LLM::Tracer, nil] :tracer
17
+ # @option options [Class] :runner_class
18
+ # @option options [String, nil] :id
19
+ # @option options [String] :name
20
+ # @option options [Hash, Array, nil] :arguments
21
+ # @option options [String, nil] :model
20
22
  # @return [LLM::Function::Ractor::Task]
21
- def initialize(runner_class, id, name, arguments, tracer: nil, span: nil)
22
- @runner_class = runner_class
23
- @id = id
24
- @name = name
25
- @arguments = arguments
26
- @tracer = tracer
27
- @span = span
23
+ def initialize(fn, options = {})
24
+ super
25
+ @runner_class = options.fetch(:runner_class)
26
+ @id = options.fetch(:id)
27
+ @name = options.fetch(:name)
28
+ @arguments = options.fetch(:arguments)
29
+ @model = options.fetch(:model, nil)
30
+ @tracer = options.fetch(:tracer, nil)
28
31
  end
29
32
 
30
33
  ##
31
34
  # @return [LLM::Function::Ractor::Task]
32
35
  def spawn
36
+ @span = @tracer&.on_tool_start(
37
+ id: @id, name: @name,
38
+ arguments: @arguments, model: @model
39
+ )
33
40
  @mailbox = Ractor::Mailbox.new(build_task)
34
41
  self
35
42
  end
@@ -37,12 +44,13 @@ class LLM::Function
37
44
  ##
38
45
  # @return [Boolean]
39
46
  def alive?
40
- mailbox.alive?
47
+ @mailbox&.alive? || false
41
48
  end
42
49
 
43
50
  ##
44
51
  # @return [nil]
45
52
  def interrupt!
53
+ mailbox&.interrupt!
46
54
  nil
47
55
  end
48
56
  alias_method :cancel!, :interrupt!
@@ -50,6 +58,7 @@ class LLM::Function
50
58
  ##
51
59
  # @return [LLM::Function::Return]
52
60
  def wait
61
+ spawn unless @mailbox
53
62
  id, name, value = mailbox.wait
54
63
  result = Return.new(id, name, value)
55
64
  @tracer&.on_tool_finish(result:, span: @span)
@@ -1,19 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Function
3
+ module LLM::Function::Sequential
4
4
  ##
5
- # The {LLM::Function::CallGroup} class wraps an array of
6
- # {LLM::Function} objects for sequential execution.
7
- #
8
- # It provides the same basic interface as the concurrent group
5
+ # Wraps an array of {LLM::Function} objects for sequential
6
+ # execution. Provides the same interface as concurrent group
9
7
  # wrappers so callers can flow through `spawn(strategy).wait`
10
- # uniformly, even when the selected strategy is direct calls.
11
- class CallGroup
8
+ # uniformly.
9
+ class Group < LLM::Function::Group
12
10
  ##
13
11
  # @param [Array<LLM::Function>] functions
14
- # @return [LLM::Function::CallGroup]
15
12
  def initialize(functions)
16
13
  @functions = functions
14
+ @owner = nil
15
+ end
16
+
17
+ ##
18
+ # @return [nil]
19
+ def spawn
20
+ # no-op — execution happens in wait
21
+ nil
17
22
  end
18
23
 
19
24
  ##
@@ -25,6 +30,7 @@ class LLM::Function
25
30
  ##
26
31
  # @return [nil]
27
32
  def interrupt!
33
+ @owner&.raise(LLM::Interrupt)
28
34
  nil
29
35
  end
30
36
  alias_method :cancel!, :interrupt!
@@ -32,7 +38,10 @@ class LLM::Function
32
38
  ##
33
39
  # @return [Array<LLM::Function::Return>]
34
40
  def wait
41
+ @owner = Thread.current
35
42
  @functions.map(&:call)
43
+ ensure
44
+ @owner = nil
36
45
  end
37
46
  alias_method :value, :wait
38
47
  end