llm.rb 7.0.0 → 8.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.
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
4
+ ##
5
+ # The {LLM::Function::Fork::Task} class wraps a fork-backed function call
6
+ # and exchanges control and result messages with the child process.
7
+ class Fork::Task
8
+ ##
9
+ # @param [LLM::Function] function
10
+ # @param [LLM::Tracer, nil] tracer
11
+ # @param [Object, nil] span
12
+ # @return [LLM::Function::Fork::Task]
13
+ def initialize(function, tracer: nil, span: nil)
14
+ @function = function
15
+ @tracer = tracer
16
+ @span = span
17
+ @waited = false
18
+ end
19
+
20
+ ##
21
+ # @return [LLM::Function::Fork::Task]
22
+ def spawn
23
+ @ch = LLM::Object.from(control: xchan(:marshal), result: xchan(:marshal))
24
+ @pid = Kernel.fork { Fork::Job.new(@function, @ch).call }
25
+ self
26
+ end
27
+
28
+ ##
29
+ # @return [Boolean]
30
+ def alive?
31
+ return false if @waited
32
+ result = ::Process.waitpid(@pid, ::Process::WNOHANG)
33
+ @waited = !result.nil?
34
+ !@waited
35
+ rescue Errno::ECHILD
36
+ @waited = true
37
+ false
38
+ end
39
+
40
+ ##
41
+ # @return [nil]
42
+ def interrupt!
43
+ return nil if @waited
44
+ @ch.control.write(:interrupt)
45
+ nil
46
+ rescue Errno::ESRCH, IOError
47
+ nil
48
+ end
49
+ alias_method :cancel!, :interrupt!
50
+
51
+ ##
52
+ # @return [LLM::Function::Return]
53
+ def wait
54
+ kind, data = @ch.result.recv
55
+ raise ArgumentError, "Unknown fork message: #{kind.inspect}" unless kind == :result
56
+ result = Return.new(data[:id], data[:name], data[:value])
57
+ reap
58
+ @tracer&.on_tool_finish(result:, span: @span)
59
+ result
60
+ ensure
61
+ reap
62
+ [@ch.control, @ch.result].each { _1.close unless _1.closed? }
63
+ end
64
+ alias_method :value, :wait
65
+
66
+ private
67
+
68
+ def reap
69
+ return if @waited
70
+ ::Process.waitpid(@pid)
71
+ @waited = true
72
+ rescue Errno::ECHILD
73
+ @waited = true
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
4
+ module Fork
5
+ require_relative "fork/job"
6
+ require_relative "fork/task"
7
+ end
8
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
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
8
+ ##
9
+ # @param [Array<LLM::Function::Task>] tasks
10
+ # @return [LLM::Function::Fork::Group]
11
+ def initialize(tasks)
12
+ @tasks = tasks
13
+ end
14
+
15
+ ##
16
+ # @return [Boolean]
17
+ def alive?
18
+ @tasks.any?(&:alive?)
19
+ end
20
+
21
+ ##
22
+ # @return [nil]
23
+ def interrupt!
24
+ @tasks.each(&:interrupt!)
25
+ nil
26
+ end
27
+ alias_method :cancel!, :interrupt!
28
+
29
+ ##
30
+ # @return [Array<LLM::Function::Return>]
31
+ def wait
32
+ @tasks.map(&:wait)
33
+ end
34
+ alias_method :value, :wait
35
+ end
36
+ end
@@ -19,9 +19,19 @@ class LLM::Function
19
19
  # @param [Object, nil] span
20
20
  # @return [LLM::Function::Ractor::Task]
21
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
22
26
  @tracer = tracer
23
27
  @span = span
24
- @mailbox = Ractor::Mailbox.new(build_task(runner_class, id, name, arguments))
28
+ end
29
+
30
+ ##
31
+ # @return [LLM::Function::Ractor::Task]
32
+ def spawn
33
+ @mailbox = Ractor::Mailbox.new(build_task)
34
+ self
25
35
  end
26
36
 
27
37
  ##
@@ -49,8 +59,8 @@ class LLM::Function
49
59
 
50
60
  private
51
61
 
52
- def build_task(runner_class, id, name, arguments)
53
- ::Ractor.new(runner_class, id, name, arguments) do |runner_class, id, name, arguments|
62
+ def build_task
63
+ ::Ractor.new(@runner_class, @id, @name, @arguments) do |runner_class, id, name, arguments|
54
64
  LLM::Function::Ractor::Job.new(::Ractor.current, runner_class, id, name, arguments).call
55
65
  end
56
66
  end
@@ -3,7 +3,8 @@
3
3
  class LLM::Function
4
4
  ##
5
5
  # The {LLM::Function::Task} class wraps a single concurrent function call and
6
- # provides a small, uniform interface across threads, fibers, and async tasks.
6
+ # provides a small, uniform interface across threads, scheduler-backed fibers,
7
+ # and async tasks.
7
8
  class Task
8
9
  ##
9
10
  # @return [Object]
@@ -32,6 +33,7 @@ class LLM::Function
32
33
  ##
33
34
  # @return [nil]
34
35
  def interrupt!
36
+ task.interrupt! if task.respond_to?(:interrupt!)
35
37
  function&.interrupt!
36
38
  nil
37
39
  end
@@ -43,12 +45,18 @@ class LLM::Function
43
45
  if Thread === task
44
46
  task.value
45
47
  elsif Fiber === task
46
- task.resume if task.alive?
48
+ fiber.alive? ? scheduler.run : nil
47
49
  task.value
48
50
  else
49
51
  task.wait
50
52
  end
51
53
  end
52
54
  alias_method :value, :wait
55
+
56
+ private
57
+
58
+ def scheduler
59
+ Fiber.scheduler
60
+ end
53
61
  end
54
62
  end
data/lib/llm/function.rb CHANGED
@@ -36,6 +36,8 @@ class LLM::Function
36
36
  require_relative "function/thread_group"
37
37
  require_relative "function/fiber_group"
38
38
  require_relative "function/task_group"
39
+ require_relative "function/fork"
40
+ require_relative "function/fork_group"
39
41
  require_relative "function/ractor"
40
42
  require_relative "function/ractor_group"
41
43
 
@@ -209,7 +211,9 @@ class LLM::Function
209
211
  # Controls concurrency strategy:
210
212
  # - `:thread`: Use threads
211
213
  # - `:task`: Use async tasks (requires async gem)
212
- # - `:fiber`: Use raw fibers
214
+ # - `:fork`: Use a forked child process (requires xchan.rb support)
215
+ # - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
216
+ # - `:fork`: Use a forked child process (requires xchan.rb support)
213
217
  # - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
214
218
  #
215
219
  # @return [LLM::Function::Task]
@@ -217,25 +221,26 @@ class LLM::Function
217
221
  def spawn(strategy)
218
222
  task = case strategy
219
223
  when :task
220
- require "async" unless defined?(::Async)
224
+ LLM.require "async" unless defined?(::Async)
221
225
  Async { call! }
222
226
  when :thread
223
227
  Thread.new { call! }
224
228
  when :fiber
225
- Fiber.new do
226
- call!
227
- ensure
228
- Fiber.yield
229
- end.tap(&:resume)
229
+ raise ArgumentError, "Fiber concurrency requires Fiber.scheduler" unless Fiber.scheduler
230
+ Fiber.schedule { call! }
231
+ when :fork
232
+ LLM.require "xchan" unless defined?(::Chan::UNIXSocket)
233
+ span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
234
+ Fork::Task.new(self, tracer: @tracer, span:).spawn
230
235
  when :ractor
231
236
  raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
232
237
  if @runner.respond_to?(:skill?) && @runner.skill?
233
238
  raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
234
239
  end
235
240
  span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
236
- Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:)
241
+ Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
237
242
  else
238
- raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
243
+ raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, :fork, or :ractor"
239
244
  end
240
245
  Task.new(task, self)
241
246
  ensure
@@ -306,6 +311,15 @@ class LLM::Function
306
311
  end
307
312
  end
308
313
 
314
+ ##
315
+ # Returns the bound function runner instance.
316
+ # @return [Object]
317
+ def runner
318
+ runner = Class === @runner ? @runner.new : @runner
319
+ runner.tracer = @tracer if runner.respond_to?(:tracer=)
320
+ runner
321
+ end
322
+
309
323
  private
310
324
 
311
325
  def format_openai(provider)
@@ -331,8 +345,7 @@ class LLM::Function
331
345
  # @return [LLM::Function::Return]
332
346
  # Returns a Return object with either the function result or error information.
333
347
  def call_function
334
- runner = ((Class === @runner) ? @runner.new : @runner)
335
- runner.tracer = @tracer if runner.respond_to?(:tracer=)
348
+ runner = self.runner
336
349
  kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
337
350
  Return.new(id, name, runner.call(**kwargs))
338
351
  rescue => ex
@@ -34,7 +34,7 @@ class LLM::MCP
34
34
  # @return [void]
35
35
  def start
36
36
  raise LLM::MCP::Error, "MCP command is already running" if alive?
37
- @stdout, @stderr, @stdin = 3.times.map { Pipe.new }
37
+ @stdout, @stderr, @stdin = 3.times.map { LLM::Pipe.new }
38
38
  @buffers.clear
39
39
  @pid = Process.spawn(env.to_h, *argv, {chdir: cwd, out: stdout.w, err: stderr.w, in: stdin.r}.compact)
40
40
  [stdin.close_reader, [stdout, stderr].each(&:close_writer)]
@@ -104,12 +104,12 @@ module LLM::MCP::Transport
104
104
  # Configures the transport to use a persistent HTTP connection pool
105
105
  # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
106
106
  # @example
107
- # mcp = LLM.mcp(http: {url: "https://example.com/mcp"}).persistent
107
+ # mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
108
108
  # # do something with 'mcp'
109
109
  # @return [LLM::MCP::Transport::HTTP]
110
110
  def persist!
111
111
  LLM.lock(:mcp) do
112
- require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
112
+ LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
113
113
  unless LLM::MCP.clients.key?(key)
114
114
  http = Net::HTTP::Persistent.new(name: self.class.name)
115
115
  http.read_timeout = timeout
data/lib/llm/mcp.rb CHANGED
@@ -19,17 +19,18 @@ class LLM::MCP
19
19
  require_relative "mcp/mailbox"
20
20
  require_relative "mcp/router"
21
21
  require_relative "mcp/rpc"
22
- require_relative "mcp/pipe"
23
22
  require_relative "mcp/transport/http"
24
23
  require_relative "mcp/transport/stdio"
25
24
 
26
25
  include RPC
27
26
 
28
- @@clients = {}
27
+ @clients = {}
29
28
 
30
29
  ##
31
30
  # @api private
32
- def self.clients = @@clients
31
+ def self.clients
32
+ @clients
33
+ end
33
34
 
34
35
  ##
35
36
  # Builds an MCP client that uses the stdio transport.
@@ -80,7 +81,9 @@ class LLM::MCP
80
81
  @command = Command.new(**stdio)
81
82
  @transport = Transport::Stdio.new(command:)
82
83
  elsif http
84
+ persistent = http.delete(:persistent)
83
85
  @transport = Transport::HTTP.new(**http, timeout:)
86
+ @transport.persistent if persistent
84
87
  else
85
88
  raise ArgumentError, "stdio or http is required"
86
89
  end
@@ -122,7 +125,7 @@ class LLM::MCP
122
125
  # Configures an HTTP MCP transport to use a persistent connection pool
123
126
  # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
124
127
  # @example
125
- # mcp = LLM.mcp(http: {url: "https://example.com/mcp"}).persistent
128
+ # mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
126
129
  # # do something with 'mcp'
127
130
  # @return [LLM::MCP]
128
131
  def persist!
@@ -4,6 +4,8 @@ class LLM::Object
4
4
  ##
5
5
  # @private
6
6
  module Kernel
7
+ TypeError = ::TypeError
8
+
7
9
  def tap(...)
8
10
  ::Kernel.instance_method(:tap).bind(self).call(...)
9
11
  end
@@ -26,11 +28,15 @@ class LLM::Object
26
28
  alias_method :is_a?, :kind_of?
27
29
 
28
30
  def respond_to?(m, include_private = false)
29
- !!key(m) || self.class.method_defined?(m)
31
+ !!SINGLETON.key(@h, m) || self.class.method_defined?(m)
30
32
  end
31
33
 
32
34
  def respond_to_missing?(m, include_private = false)
33
- !!key(m)
35
+ !!SINGLETON.key(@h, m)
36
+ end
37
+
38
+ def raise(...)
39
+ ::Kernel.raise(...)
34
40
  end
35
41
 
36
42
  def object_id
data/lib/llm/object.rb CHANGED
@@ -8,6 +8,37 @@ class LLM::Object < BasicObject
8
8
  require_relative "object/builder"
9
9
  require_relative "object/kernel"
10
10
 
11
+ SINGLETON = self
12
+ UNDEFINED = ::Object.new.freeze
13
+ LLM = ::LLM
14
+ private_constant :SINGLETON, :UNDEFINED, :LLM
15
+
16
+ ##
17
+ # @api private
18
+ # @param [Hash] h
19
+ # @param [#to_s, #to_sym] k
20
+ # @return [String, Symbol, nil]
21
+ def self.key(h, k)
22
+ return nil if k.nil?
23
+ if h.key?(k.to_s)
24
+ k.to_s
25
+ elsif h.key?(k.to_sym)
26
+ k.to_sym
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ ##
33
+ # @api private
34
+ # @param [Hash] h
35
+ # @param [#to_s, #to_sym] k
36
+ # @return [Object, nil]
37
+ def self.get(h, k)
38
+ name = key(h, k)
39
+ h[name] if name
40
+ end
41
+
11
42
  extend Builder
12
43
  include Kernel
13
44
  include ::Enumerable
@@ -33,7 +64,7 @@ class LLM::Object < BasicObject
33
64
  # @param [Symbol, #to_sym] k
34
65
  # @return [Object]
35
66
  def [](k)
36
- @h[key(k)]
67
+ @h[SINGLETON.key(@h, k)]
37
68
  end
38
69
 
39
70
  ##
@@ -47,7 +78,7 @@ class LLM::Object < BasicObject
47
78
  ##
48
79
  # @return [String]
49
80
  def to_json(...)
50
- to_h.to_json(...)
81
+ LLM.json.dump(to_h, ...)
51
82
  end
52
83
 
53
84
  ##
@@ -83,16 +114,39 @@ class LLM::Object < BasicObject
83
114
  ##
84
115
  # @param [String, Symbol] k
85
116
  # @return [Boolean]
86
- def key?(k)
87
- @h.key?(key(k))
117
+ def key?(k = UNDEFINED)
118
+ return SINGLETON.get(@h, :key?) if k.equal?(UNDEFINED)
119
+ @h.key?(SINGLETON.key(@h, k))
88
120
  end
89
121
  alias_method :has_key?, :key?
90
122
 
91
123
  ##
92
124
  # @param [String, Symbol] k
93
125
  # @return [Object]
94
- def fetch(k, *args, &b)
95
- @h.fetch(key(k), *args, &b)
126
+ def fetch(k = UNDEFINED, *args, &b)
127
+ return SINGLETON.get(@h, :fetch) if k.equal?(UNDEFINED)
128
+ @h.fetch(SINGLETON.key(@h, k), *args, &b)
129
+ end
130
+
131
+ ##
132
+ # @param [Hash, to_h] other
133
+ # The hash to merge
134
+ # @return [LLM::Object]
135
+ # Returns a new LLM::Object
136
+ def merge(other = UNDEFINED)
137
+ return SINGLETON.get(@h, :merge) if other.equal?(UNDEFINED)
138
+ other = ::Hash.try_convert(other)
139
+ raise TypeError, "#{other} cannot be coerced into a Hash" unless other
140
+ SINGLETON.from @h.merge(other)
141
+ end
142
+
143
+ ##
144
+ # @param [#to_s, #to_sym] k
145
+ # The key name
146
+ # @return [void]
147
+ def delete(k = UNDEFINED)
148
+ return SINGLETON.get(@h, :delete) if k.equal?(UNDEFINED)
149
+ @h.delete(SINGLETON.key(@h, k))
96
150
  end
97
151
 
98
152
  ##
@@ -110,14 +164,16 @@ class LLM::Object < BasicObject
110
164
 
111
165
  ##
112
166
  # @return [Object, nil]
113
- def dig(...)
114
- @h.dig(...)
167
+ def dig(*args)
168
+ return SINGLETON.get(@h, :dig) if args.empty?
169
+ @h.dig(*args)
115
170
  end
116
171
 
117
172
  ##
118
173
  # @return [Hash]
119
- def slice(...)
120
- @h.slice(...)
174
+ def slice(*args)
175
+ return SINGLETON.get(@h, :slice) if args.empty?
176
+ @h.slice(*args)
121
177
  end
122
178
 
123
179
  private
@@ -125,20 +181,10 @@ class LLM::Object < BasicObject
125
181
  def method_missing(m, *args, &b)
126
182
  if m.to_s.end_with?("=")
127
183
  self[m[0..-2]] = args.first
128
- elsif k = key(m)
184
+ elsif k = SINGLETON.key(@h, m)
129
185
  @h[k]
130
186
  else
131
187
  nil
132
188
  end
133
189
  end
134
-
135
- def key(k)
136
- if @h.key?(k.to_s)
137
- k.to_s
138
- elsif @h.key?(k.to_sym)
139
- k.to_sym
140
- else
141
- nil
142
- end
143
- end
144
190
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::MCP
3
+ module LLM
4
4
  ##
5
- # The {LLM::MCP::Pipe LLM::MCP::Pipe} class wraps a pair of IO
6
- # objects created by {IO.pipe}. It is used by
7
- # {LLM::MCP::Transport::Stdio LLM::MCP::Transport::Stdio} to manage
8
- # the stdin, stdout, and stderr streams of an MCP process through
9
- # one small interface.
5
+ # The {LLM::Pipe LLM::Pipe} class wraps a pair of IO objects created by
6
+ # {IO.pipe}. It is used by llm.rb internals to manage process and stream
7
+ # communication through one small interface.
10
8
  class Pipe
11
9
  ##
12
10
  # @return [IO]
@@ -20,9 +18,12 @@ class LLM::MCP
20
18
 
21
19
  ##
22
20
  # Returns a new pipe.
23
- # @return [LLM::MCP::Pipe]
24
- def initialize
21
+ # @param [Boolean] binmode
22
+ # Whether both ends of the pipe should be switched to binary mode
23
+ # @return [LLM::Pipe]
24
+ def initialize(binmode: false)
25
25
  @r, @w = IO.pipe
26
+ [@r, @w].each(&:binmode) if binmode
26
27
  end
27
28
 
28
29
  ##
@@ -53,7 +53,7 @@ class LLM::Provider
53
53
  # @return [Object]
54
54
  def request_owner
55
55
  return Fiber.current unless defined?(::Async)
56
- Async::Task.current || Fiber.current
56
+ Async::Task.current? ? Async::Task.current : Fiber.current
57
57
  end
58
58
 
59
59
  ##
@@ -46,7 +46,7 @@ class LLM::Stream
46
46
  # to wait on:
47
47
  # - `:thread`: Use threads
48
48
  # - `:task`: Use async tasks (requires async gem)
49
- # - `:fiber`: Use raw fibers
49
+ # - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
50
50
  # - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
51
51
  # - `[:thread, :ractor]`: Wait for any queued thread or ractor work, in the
52
52
  # given order. This is useful when different tools were spawned with
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "7.0.0"
4
+ VERSION = "8.0.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -20,6 +20,7 @@ module LLM
20
20
  require_relative "llm/mime"
21
21
  require_relative "llm/multipart"
22
22
  require_relative "llm/file"
23
+ require_relative "llm/pipe"
23
24
  require_relative "llm/stream"
24
25
  require_relative "llm/provider"
25
26
  require_relative "llm/context"
@@ -48,7 +49,24 @@ module LLM
48
49
 
49
50
  ##
50
51
  # @api private
51
- def self.clients = @clients
52
+ def self.clients
53
+ @clients
54
+ end
55
+
56
+ ##
57
+ # Requires an optional runtime dependency
58
+ # @raise [LLM::DependencyError]
59
+ # When the dependency cannot be loaded
60
+ def self.require(name)
61
+ super
62
+ rescue ::LoadError
63
+ names = {"xchan" => "xchan.rb", "net/http/persistent" => "net-http-persistent"}
64
+ name = names[name] || name
65
+ raise LLM::LoadError,
66
+ "#{name} is an optional runtime dependency but it does not appear to be installed. " \
67
+ "Consider 'gem install #{name}', adding '#{name}' to your Gemfile or " \
68
+ "opting out of the functionality provided by '#{name}'"
69
+ end
52
70
 
53
71
  ##
54
72
  # @param [Symbol, LLM::Provider] llm
data/llm.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  DESCRIPTION
26
26
 
27
27
  spec.license = "0BSD"
28
- spec.required_ruby_version = ">= 3.2.0"
28
+ spec.required_ruby_version = ">= 3.3.0"
29
29
 
30
30
  spec.homepage = "https://github.com/llmrb/llm.rb"
31
31
  spec.metadata["homepage_uri"] = "https://github.com/llmrb/llm.rb"
@@ -57,5 +57,6 @@ Gem::Specification.new do |spec|
57
57
  spec.add_development_dependency "activerecord", "~> 8.0"
58
58
  spec.add_development_dependency "sequel", "~> 5.0"
59
59
  spec.add_development_dependency "sqlite3", "~> 2.0"
60
+ spec.add_development_dependency "xchan.rb", "~> 0.20"
60
61
  spec.add_development_dependency "pg", "~> 1.5"
61
62
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -236,6 +236,20 @@ dependencies:
236
236
  - - "~>"
237
237
  - !ruby/object:Gem::Version
238
238
  version: '2.0'
239
+ - !ruby/object:Gem::Dependency
240
+ name: xchan.rb
241
+ requirement: !ruby/object:Gem::Requirement
242
+ requirements:
243
+ - - "~>"
244
+ - !ruby/object:Gem::Version
245
+ version: '0.20'
246
+ type: :development
247
+ prerelease: false
248
+ version_requirements: !ruby/object:Gem::Requirement
249
+ requirements:
250
+ - - "~>"
251
+ - !ruby/object:Gem::Version
252
+ version: '0.20'
239
253
  - !ruby/object:Gem::Dependency
240
254
  name: pg
241
255
  requirement: !ruby/object:Gem::Requirement
@@ -301,6 +315,10 @@ files:
301
315
  - lib/llm/function.rb
302
316
  - lib/llm/function/array.rb
303
317
  - lib/llm/function/fiber_group.rb
318
+ - lib/llm/function/fork.rb
319
+ - lib/llm/function/fork/job.rb
320
+ - lib/llm/function/fork/task.rb
321
+ - lib/llm/function/fork_group.rb
304
322
  - lib/llm/function/ractor.rb
305
323
  - lib/llm/function/ractor/job.rb
306
324
  - lib/llm/function/ractor/mailbox.rb
@@ -317,7 +335,6 @@ files:
317
335
  - lib/llm/mcp/command.rb
318
336
  - lib/llm/mcp/error.rb
319
337
  - lib/llm/mcp/mailbox.rb
320
- - lib/llm/mcp/pipe.rb
321
338
  - lib/llm/mcp/router.rb
322
339
  - lib/llm/mcp/rpc.rb
323
340
  - lib/llm/mcp/transport/http.rb
@@ -331,6 +348,7 @@ files:
331
348
  - lib/llm/object.rb
332
349
  - lib/llm/object/builder.rb
333
350
  - lib/llm/object/kernel.rb
351
+ - lib/llm/pipe.rb
334
352
  - lib/llm/prompt.rb
335
353
  - lib/llm/provider.rb
336
354
  - lib/llm/provider/transport/http.rb
@@ -464,7 +482,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
464
482
  requirements:
465
483
  - - ">="
466
484
  - !ruby/object:Gem::Version
467
- version: 3.2.0
485
+ version: 3.3.0
468
486
  required_rubygems_version: !ruby/object:Gem::Requirement
469
487
  requirements:
470
488
  - - ">="