llm.rb 9.0.0 → 11.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -4
  3. data/README.md +194 -42
  4. data/data/anthropic.json +278 -258
  5. data/data/bedrock.json +1288 -1561
  6. data/data/deepseek.json +38 -38
  7. data/data/google.json +656 -579
  8. data/data/openai.json +860 -818
  9. data/data/xai.json +243 -552
  10. data/data/zai.json +168 -168
  11. data/lib/llm/a2a/card/capabilities.rb +41 -0
  12. data/lib/llm/a2a/card/interface.rb +34 -0
  13. data/lib/llm/a2a/card/provider.rb +27 -0
  14. data/lib/llm/a2a/card/skill.rb +68 -0
  15. data/lib/llm/a2a/card.rb +144 -0
  16. data/lib/llm/a2a/error.rb +49 -0
  17. data/lib/llm/a2a/notifications.rb +53 -0
  18. data/lib/llm/a2a/tasks.rb +55 -0
  19. data/lib/llm/a2a/transport/http.rb +131 -0
  20. data/lib/llm/a2a.rb +452 -0
  21. data/lib/llm/active_record/acts_as_agent.rb +20 -9
  22. data/lib/llm/active_record/acts_as_llm.rb +4 -4
  23. data/lib/llm/active_record.rb +1 -6
  24. data/lib/llm/agent.rb +96 -71
  25. data/lib/llm/buffer.rb +1 -2
  26. data/lib/llm/context.rb +77 -50
  27. data/lib/llm/file.rb +7 -0
  28. data/lib/llm/function/call_task.rb +46 -0
  29. data/lib/llm/function.rb +28 -2
  30. data/lib/llm/mcp/transport/http.rb +5 -18
  31. data/lib/llm/mcp/transport/stdio.rb +7 -0
  32. data/lib/llm/mcp.rb +20 -17
  33. data/lib/llm/message.rb +1 -1
  34. data/lib/llm/object/kernel.rb +1 -1
  35. data/lib/llm/provider.rb +9 -9
  36. data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
  37. data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
  38. data/lib/llm/providers/google/stream_parser.rb +2 -2
  39. data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
  40. data/lib/llm/providers/openai/stream_parser.rb +2 -2
  41. data/lib/llm/response.rb +1 -1
  42. data/lib/llm/schema.rb +11 -0
  43. data/lib/llm/sequel/agent.rb +19 -9
  44. data/lib/llm/sequel/plugin.rb +9 -13
  45. data/lib/llm/stream.rb +11 -36
  46. data/lib/llm/tool/param.rb +1 -8
  47. data/lib/llm/tool.rb +57 -27
  48. data/lib/llm/tracer.rb +1 -1
  49. data/lib/llm/transport/http.rb +1 -1
  50. data/lib/llm/transport/stream_decoder.rb +6 -3
  51. data/lib/llm/transport/utils.rb +35 -0
  52. data/lib/llm/transport.rb +1 -0
  53. data/lib/llm/utils.rb +73 -0
  54. data/lib/llm/version.rb +1 -1
  55. data/lib/llm.rb +24 -4
  56. data/llm.gemspec +16 -1
  57. metadata +29 -5
  58. data/lib/llm/bot.rb +0 -3
  59. data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
@@ -78,6 +78,13 @@ module LLM::MCP::Transport
78
78
  command.wait
79
79
  end
80
80
 
81
+ ##
82
+ # @return [Boolean]
83
+ # Returns true when the MCP server connection is alive
84
+ def running?
85
+ command.alive?
86
+ end
87
+
81
88
  private
82
89
 
83
90
  attr_reader :command, :stdin, :stdout, :stderr
data/lib/llm/mcp.rb CHANGED
@@ -26,29 +26,23 @@ class LLM::MCP
26
26
 
27
27
  ##
28
28
  # Builds an MCP client that uses the stdio transport.
29
- # @param [LLM::Provider, nil] llm
30
- # An instance of LLM::Provider. Optional.
31
29
  # @param [Hash] stdio
32
30
  # The stdio transport configuration
33
31
  # @return [LLM::MCP]
34
- def self.stdio(llm = nil, **stdio)
35
- new(llm, stdio:)
32
+ def self.stdio(**stdio)
33
+ new(stdio:)
36
34
  end
37
35
 
38
36
  ##
39
37
  # Builds an MCP client that uses the HTTP transport.
40
- # @param [LLM::Provider, nil] llm
41
- # An instance of LLM::Provider. Optional.
42
38
  # @param [Hash] http
43
39
  # The HTTP transport configuration
44
40
  # @return [LLM::MCP]
45
- def self.http(llm = nil, **http)
46
- new(llm, http:)
41
+ def self.http(**http)
42
+ new(http:)
47
43
  end
48
44
 
49
45
  ##
50
- # @param [LLM::Provider, nil] llm
51
- # The provider to use for MCP transports that need one
52
46
  # @param [Hash, nil] stdio The configuration for the stdio transport
53
47
  # @option stdio [Array<String>] :argv
54
48
  # The command to run for the MCP process
@@ -67,8 +61,7 @@ class LLM::MCP
67
61
  # @param [Integer] timeout
68
62
  # The maximum amount of time to wait when reading from an MCP process
69
63
  # @return [LLM::MCP] A new MCP instance
70
- def initialize(llm = nil, stdio: nil, http: nil, timeout: 30)
71
- @llm = llm
64
+ def initialize(stdio: nil, http: nil, timeout: 30)
72
65
  @timeout = timeout
73
66
  if stdio && http
74
67
  raise ArgumentError, "stdio and http are mutually exclusive"
@@ -116,12 +109,13 @@ class LLM::MCP
116
109
  ensure
117
110
  stop
118
111
  end
112
+ alias_method :session, :run
119
113
 
120
114
  ##
121
115
  # Returns the tools provided by the MCP process.
122
116
  # @return [Array<Class<LLM::Tool>>]
123
117
  def tools
124
- res = call(transport, "tools/list")
118
+ res = with_session { call(transport, "tools/list") }
125
119
  res["tools"].map { LLM::Tool.mcp(self, _1) }
126
120
  end
127
121
 
@@ -129,7 +123,7 @@ class LLM::MCP
129
123
  # Returns the prompts provided by the MCP process.
130
124
  # @return [Array<LLM::Object>]
131
125
  def prompts
132
- res = call(transport, "prompts/list")
126
+ res = with_session { call(transport, "prompts/list") }
133
127
  LLM::Object.from(res["prompts"])
134
128
  end
135
129
 
@@ -141,7 +135,7 @@ class LLM::MCP
141
135
  def find_prompt(name:, arguments: nil)
142
136
  params = {name:}
143
137
  params[:arguments] = arguments if arguments
144
- res = call(transport, "prompts/get", params)
138
+ res = with_session { call(transport, "prompts/get", params) }
145
139
  res["messages"] = [*res["messages"]].map do |message|
146
140
  LLM::Message.new(
147
141
  message["role"],
@@ -159,13 +153,22 @@ class LLM::MCP
159
153
  # @param [Hash] arguments The arguments to pass to the tool
160
154
  # @return [Object] The result of the tool call
161
155
  def call_tool(name, arguments = {})
162
- res = call(transport, "tools/call", {name:, arguments:})
156
+ res = with_session { call(transport, "tools/call", {name:, arguments:}) }
163
157
  adapt_tool_result(res)
164
158
  end
165
159
 
166
160
  private
167
161
 
168
- attr_reader :llm, :command, :transport, :timeout
162
+ attr_reader :command, :transport, :timeout
163
+
164
+ def with_session
165
+ return yield if transport.running?
166
+ session_started = true
167
+ start
168
+ yield
169
+ ensure
170
+ stop if session_started
171
+ end
169
172
 
170
173
  def adapt_content(content)
171
174
  case content
data/lib/llm/message.rb CHANGED
@@ -205,7 +205,7 @@ module LLM
205
205
  # Returns a string representation of the message
206
206
  # @return [String]
207
207
  def inspect
208
- "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
208
+ "#<#{LLM::Utils.object_id(self)} " \
209
209
  "tool_call=#{tool_calls.any?} role=#{role.inspect} " \
210
210
  "content=#{content.inspect} reasoning_content=#{reasoning_content.inspect}>"
211
211
  end
@@ -48,7 +48,7 @@ class LLM::Object
48
48
  end
49
49
 
50
50
  def inspect
51
- "#<#{self.class}:0x#{object_id.to_s(16)} properties=#{to_h.inspect}>"
51
+ "#<#{LLM::Utils.object_id(self)} properties=#{to_h.inspect}>"
52
52
  end
53
53
  alias_method :to_s, :inspect
54
54
 
data/lib/llm/provider.rb CHANGED
@@ -32,7 +32,7 @@ class LLM::Provider
32
32
  @port = port
33
33
  @timeout = timeout
34
34
  @ssl = ssl
35
- @base_path = normalize_base_path(base_path)
35
+ @base_path = LLM::Utils.normalize_base_path(base_path)
36
36
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
37
37
  @headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
38
38
  @transport = resolve_transport(transport, persistent:)
@@ -44,7 +44,7 @@ class LLM::Provider
44
44
  # @return [String]
45
45
  # @note The secret key is redacted in inspect for security reasons
46
46
  def inspect
47
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @transport=#{transport.inspect} @tracer=#{tracer.inspect}>"
47
+ "#<#{LLM::Utils.object_id(self)} @key=[REDACTED] @transport=#{transport.inspect} @tracer=#{tracer.inspect}>"
48
48
  end
49
49
 
50
50
  ##
@@ -339,6 +339,13 @@ class LLM::Provider
339
339
  LLM::Stream === stream || stream.respond_to?(:<<)
340
340
  end
341
341
 
342
+ ##
343
+ # @return [Boolean]
344
+ # Returns true when an API key is configured
345
+ def key?
346
+ @key != nil && @key.to_s.strip.size > 0
347
+ end
348
+
342
349
  private
343
350
 
344
351
  def path(suffix)
@@ -346,13 +353,6 @@ class LLM::Provider
346
353
  "#{@base_path}#{suffix}"
347
354
  end
348
355
 
349
- def normalize_base_path(path)
350
- path = path.to_s.strip
351
- return "" if path.empty? || path == "/"
352
- path = "/#{path}" unless path.start_with?("/")
353
- path.sub(%r{/+\z}, "")
354
- end
355
-
356
356
  attr_reader :base_uri, :host, :port, :timeout, :ssl, :transport
357
357
 
358
358
  ##
@@ -105,14 +105,14 @@ class LLM::Anthropic
105
105
  end
106
106
 
107
107
  def resolve_tool(tool)
108
- registered = @stream.find_tool(tool["name"])
108
+ registered = @stream.__find__(tool["name"])
109
109
  fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
110
110
  fn.id = tool["id"]
111
111
  fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
112
112
  fn.tracer = @stream.extra[:tracer]
113
113
  fn.model = @stream.extra[:model]
114
114
  end
115
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
115
+ [fn, (registered ? nil : fn.unavailable)]
116
116
  end
117
117
  end
118
118
  end
@@ -184,14 +184,14 @@ class LLM::Bedrock
184
184
 
185
185
  def resolve_tool(tool)
186
186
  payload = tool["toolUse"] || {}
187
- registered = @stream.find_tool(payload["name"])
187
+ registered = @stream.__find__(payload["name"])
188
188
  fn = (registered || LLM::Function.new(payload["name"])).dup.tap do |f|
189
189
  f.id = payload["toolUseId"]
190
190
  f.arguments = payload["input"] || {}
191
191
  f.tracer = @stream.extra[:tracer]
192
192
  f.model = @stream.extra[:model]
193
193
  end
194
- [fn, registered ? nil : @stream.tool_not_found(fn)]
194
+ [fn, registered ? nil : fn.unavailable]
195
195
  end
196
196
 
197
197
  def content
@@ -153,14 +153,14 @@ class LLM::Google
153
153
 
154
154
  def resolve_tool(part, cindex, pindex)
155
155
  call = part["functionCall"]
156
- registered = @stream.find_tool(call["name"])
156
+ registered = @stream.__find__(call["name"])
157
157
  fn = (registered || LLM::Function.new(call["name"])).dup.tap do |fn|
158
158
  fn.id = LLM::Google.tool_id(part:, cindex:, pindex:)
159
159
  fn.arguments = call["args"]
160
160
  fn.tracer = @stream.extra[:tracer]
161
161
  fn.model = @stream.extra[:model]
162
162
  end
163
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
163
+ [fn, (registered ? nil : fn.unavailable)]
164
164
  end
165
165
  end
166
166
  end
@@ -269,14 +269,14 @@ class LLM::OpenAI
269
269
  # @group Resolvers
270
270
 
271
271
  def resolve_tool(tool, arguments)
272
- registered = @stream.find_tool(tool["name"])
272
+ registered = @stream.__find__(tool["name"])
273
273
  fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
274
274
  fn.id = tool["call_id"]
275
275
  fn.arguments = arguments
276
276
  fn.tracer = @stream.extra[:tracer]
277
277
  fn.model = @stream.extra[:model]
278
278
  end
279
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
279
+ [fn, (registered ? nil : fn.unavailable)]
280
280
  end
281
281
 
282
282
  def parse_arguments(arguments)
@@ -185,14 +185,14 @@ class LLM::OpenAI
185
185
  end
186
186
 
187
187
  def resolve_tool(tool, function, arguments)
188
- registered = @stream.find_tool(function["name"])
188
+ registered = @stream.__find__(function["name"])
189
189
  fn = (registered || LLM::Function.new(function["name"])).dup.tap do |fn|
190
190
  fn.id = tool["id"]
191
191
  fn.arguments = arguments
192
192
  fn.tracer = @stream.extra[:tracer]
193
193
  fn.model = @stream.extra[:model]
194
194
  end
195
- [fn, (registered ? nil : @stream.tool_not_found(fn))]
195
+ [fn, (registered ? nil : fn.unavailable)]
196
196
  end
197
197
 
198
198
  def parse_arguments(arguments)
data/lib/llm/response.rb CHANGED
@@ -46,7 +46,7 @@ module LLM
46
46
  # Returns an inspection of the response object
47
47
  # @return [String]
48
48
  def inspect
49
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @body=#{body.inspect} @res=#{@res.inspect}>"
49
+ "#<#{LLM::Utils.object_id(self)} @body=#{body.inspect} @res=#{@res.inspect}>"
50
50
  end
51
51
 
52
52
  ##
data/lib/llm/schema.rb CHANGED
@@ -56,6 +56,8 @@ class LLM::Schema
56
56
  def resolve(schema, type)
57
57
  if LLM::Schema::Leaf === type
58
58
  type
59
+ elsif ::Array === type
60
+ resolve_array(schema, type)
59
61
  elsif Class === type && type.respond_to?(:object)
60
62
  type.object
61
63
  else
@@ -63,6 +65,15 @@ class LLM::Schema
63
65
  schema.public_send(target)
64
66
  end
65
67
  end
68
+
69
+ def resolve_array(schema, values)
70
+ item = if values.size == 1
71
+ resolve(schema, values[0])
72
+ else
73
+ schema.any_of(*values.map { resolve(schema, _1) })
74
+ end
75
+ schema.array(item)
76
+ end
66
77
  end
67
78
 
68
79
  ##
@@ -33,19 +33,24 @@ module LLM::Sequel
33
33
  @llm_agent_options || Agent::DEFAULTS
34
34
  end
35
35
 
36
- def model(model = nil)
37
- return agent.model if model.nil?
38
- agent.model(model)
36
+ def model(model = nil, &block)
37
+ return agent.model if model.nil? && !block
38
+ agent.model(model, &block)
39
39
  end
40
40
 
41
- def tools(*tools)
42
- return agent.tools if tools.empty?
43
- agent.tools(*tools)
41
+ def tools(*tools, &block)
42
+ return agent.tools if tools.empty? && !block
43
+ agent.tools(*tools, &block)
44
44
  end
45
45
 
46
- def schema(schema = nil)
47
- return agent.schema if schema.nil?
48
- agent.schema(schema)
46
+ def skills(*skills, &block)
47
+ return agent.skills if skills.empty? && !block
48
+ agent.skills(*skills, &block)
49
+ end
50
+
51
+ def schema(schema = nil, &block)
52
+ return agent.schema if schema.nil? && !block
53
+ agent.schema(schema, &block)
49
54
  end
50
55
 
51
56
  def instructions(instructions = nil)
@@ -58,6 +63,11 @@ module LLM::Sequel
58
63
  agent.concurrency(concurrency)
59
64
  end
60
65
 
66
+ def confirm(*tool_names, &block)
67
+ return agent.confirm if tool_names.empty? && !block
68
+ agent.confirm(*tool_names, &block)
69
+ end
70
+
61
71
  def tracer(tracer = nil, &block)
62
72
  return agent.tracer if tracer.nil? && !block
63
73
  agent.tracer(tracer, &block)
@@ -30,12 +30,7 @@ module LLM::Sequel
30
30
  # Resolves a single configured option against a model instance.
31
31
  # @return [Object]
32
32
  def self.resolve_option(obj, option)
33
- case option
34
- when Proc then obj.instance_exec(&option)
35
- when Symbol then obj.send(option)
36
- when Hash then option.dup
37
- else option
38
- end
33
+ LLM::Utils.resolve_option(obj, option)
39
34
  end
40
35
 
41
36
  ##
@@ -80,11 +75,12 @@ module LLM::Sequel
80
75
  ##
81
76
  # Persists the runtime state and usage columns back onto the record.
82
77
  # @return [void]
83
- def self.save(obj, ctx, options)
78
+ def self.save!(obj, ctx, options)
84
79
  columns = self.columns(options)
85
80
  payload = serialize_context(ctx, options[:format])
86
81
  payload = wrap_json_payload(payload, options[:format])
87
- obj.update(columns[:data_column] => payload)
82
+ obj[columns[:data_column]] = payload
83
+ obj.save_changes(raise_on_failure: true)
88
84
  end
89
85
 
90
86
  ##
@@ -164,16 +160,16 @@ module LLM::Sequel
164
160
  # @return [LLM::Response]
165
161
  def talk(...)
166
162
  options = self.class.llm_plugin_options
167
- ctx.talk(...).tap { Utils.save(self, ctx, options) }
163
+ ctx.talk(...).tap { Utils.save!(self, ctx, options) }
168
164
  end
169
165
 
170
166
  ##
171
- # Continues the stored context through the Responses API and flushes it.
172
- # @see LLM::Context#respond
167
+ # Continues the stored context with new input and flushes it.
168
+ # @see LLM::Context#ask
173
169
  # @return [LLM::Response]
174
- def respond(...)
170
+ def ask(...)
175
171
  options = self.class.llm_plugin_options
176
- ctx.respond(...).tap { Utils.save(self, ctx, options) }
172
+ ctx.ask(...).tap { Utils.save!(self, ctx, options) }
177
173
  end
178
174
 
179
175
  ##
data/lib/llm/stream.rb CHANGED
@@ -9,8 +9,7 @@ module LLM
9
9
  # subclass that overrides the callbacks it needs. For basic streaming,
10
10
  # llm.rb also accepts any object that implements `#<<`. {#queue} provides
11
11
  # a small helper for collecting asynchronous tool work started from a
12
- # callback, and {#tool_not_found} returns an in-band tool error when a
13
- # streamed tool cannot be resolved.
12
+ # callback.
14
13
  #
15
14
  # @note The `on_*` callbacks run inline with the streaming parser. They
16
15
  # therefore block streaming progress and should generally return as
@@ -150,48 +149,24 @@ module LLM
150
149
 
151
150
  # @endgroup
152
151
 
153
- # @group Error handlers
154
-
155
- ##
156
- # Returns a function return describing a streamed tool that could not
157
- # be resolved.
158
- # @note This is mainly useful as a fallback from {#on_tool_call}. It
159
- # should be uncommon in normal use, since streamed tool callbacks only
160
- # run for tools already defined in the context.
161
- # @param [LLM::Function] tool
162
- # @return [LLM::Function::Return]
163
- def tool_not_found(tool)
164
- LLM::Function::Return.new(tool.id, tool.name, {
165
- error: true, type: LLM::NoSuchToolError.name, message: "tool not found"
166
- })
167
- end
168
-
169
- ##
170
- # Returns the tool definitions available for the current streamed request.
171
- # This prefers request-local tools attached to the stream and falls back
172
- # to the current context defaults when present.
173
- # @return [Array<LLM::Function, LLM::Tool>]
174
- def tools
175
- extra[:tools] || ctx&.params&.dig(:tools) || []
176
- end
152
+ # @group Finders
177
153
 
178
154
  ##
179
155
  # Resolves a streamed tool call against the current request tools first,
180
156
  # then falls back to the global function registry.
181
157
  # @param [String] name
182
158
  # @return [LLM::Function, nil]
183
- def find_tool(name)
184
- tool = tools.find do |candidate|
185
- candidate_name =
186
- if candidate.respond_to?(:function)
187
- candidate.function.name
188
- else
189
- candidate.name
190
- end
191
- candidate_name.to_s == name.to_s
159
+ def __find__(name)
160
+ tools = extra[:tools] || ctx&.params&.dig(:tools) || []
161
+ tool = tools.find do
162
+ candidate = _1.respond_to?(:function) ? _1.function.name : _1.name
163
+ candidate.to_s == name.to_s
192
164
  end
193
- tool&.then { _1.respond_to?(:function) ? _1.function : _1 } ||
165
+ if tool
166
+ tool.respond_to?(:function) ? tool.function : tool
167
+ else
194
168
  LLM::Function.find_by_name(name)
169
+ end
195
170
  end
196
171
 
197
172
  # @endgroup
@@ -62,14 +62,7 @@ class LLM::Tool
62
62
  extend self
63
63
 
64
64
  def resolve(schema, type)
65
- if LLM::Schema::Leaf === type
66
- type
67
- elsif Class === type && type.respond_to?(:object)
68
- type.object
69
- else
70
- target = type.name.split("::").last.downcase
71
- schema.public_send(target)
72
- end
65
+ LLM::Schema::Utils.resolve(schema, type)
73
66
  end
74
67
 
75
68
  def setup(leaf, description, options)
data/lib/llm/tool.rb CHANGED
@@ -40,31 +40,54 @@ class LLM::Tool
40
40
  # @return [Class<LLM::Tool>]
41
41
  # Returns a subclass of LLM::Tool
42
42
  def self.mcp(mcp, tool)
43
- lock do
44
- @mcp = true
45
- klass = Class.new(LLM::Tool) do
46
- name tool["name"]
47
- description tool["description"]
48
- params { tool["inputSchema"] || {type: "object", properties: {}} }
49
-
50
- define_singleton_method(:inspect) do
51
- "<LLM::Tool:0x#{object_id.to_s(16)} name=#{tool["name"]} (mcp)>"
52
- end
53
- singleton_class.alias_method :to_s, :inspect
54
-
55
- define_singleton_method(:mcp?) do
56
- true
57
- end
58
-
59
- define_method(:call) do |**args|
60
- mcp.call_tool(tool["name"], args)
61
- end
43
+ Class.new(LLM::Tool) do
44
+ name tool["name"]
45
+ description tool["description"]
46
+ params { tool["inputSchema"] || {type: "object", properties: {}} }
47
+
48
+ define_singleton_method(:inspect) do
49
+ "<#{LLM::Utils.object_id(self)} name=#{tool["name"]} (mcp)>"
50
+ end
51
+ singleton_class.alias_method :to_s, :inspect
52
+
53
+ define_singleton_method(:mcp?) do
54
+ true
55
+ end
56
+
57
+ define_method(:call) do |**args|
58
+ mcp.call_tool(tool["name"], args)
59
+ end
60
+ end
61
+ end
62
+
63
+ ##
64
+ # @param [LLM::A2A] a2a
65
+ # The A2A client that will execute the tool call
66
+ # @param [LLM::A2A::Card::Skill]
67
+ # An A2A tool
68
+ # @return [Class<LLM::Tool>]
69
+ # Returns a subclass of LLM::Tool
70
+ def self.a2a(a2a, skill)
71
+ name = skill.name.gsub(" ", "-")
72
+ Class.new(LLM::Tool) do
73
+ name(name)
74
+ description(skill.description)
75
+ parameter :input, String, "The input string"
76
+ required %i[input]
77
+
78
+ define_singleton_method(:inspect) do
79
+ "<#{LLM::Utils.object_id(self)} name=#{name} (a2a)>"
80
+ end
81
+ singleton_class.alias_method :to_s, :inspect
82
+
83
+ define_singleton_method(:a2a?) do
84
+ true
85
+ end
86
+
87
+ define_method(:call) do |input:|
88
+ res = a2a.send_message(input)
89
+ {task: res}
62
90
  end
63
- @mcp = false
64
- register(klass)
65
- klass
66
- ensure
67
- @mcp = false
68
91
  end
69
92
  end
70
93
 
@@ -106,8 +129,8 @@ class LLM::Tool
106
129
  def self.inherited(tool)
107
130
  LLM.lock(:inherited) do
108
131
  tool.instance_eval { @__monitor ||= Monitor.new }
109
- tool.function.register(tool)
110
- LLM::Tool.register(tool) unless lock { @mcp }
132
+ tool.function.define(tool)
133
+ LLM::Tool.register(tool) unless tool.mcp? || tool.a2a?
111
134
  end
112
135
  end
113
136
 
@@ -172,11 +195,18 @@ class LLM::Tool
172
195
  false
173
196
  end
174
197
 
198
+ ##
199
+ # Returns true if the tool is an A2A tool
200
+ # @return [Boolean]
201
+ def self.a2a?
202
+ false
203
+ end
204
+
175
205
  ##
176
206
  # Returns a function bound to this tool instance.
177
207
  # @return [LLM::Function]
178
208
  def function
179
- @function ||= self.class.function.dup.tap { _1.register(self) }
209
+ @function ||= self.class.function.dup.tap { _1.define(self) }
180
210
  end
181
211
 
182
212
  ##
data/lib/llm/tracer.rb CHANGED
@@ -128,7 +128,7 @@ module LLM
128
128
  ##
129
129
  # @return [String]
130
130
  def inspect
131
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @provider=#{@llm.class} @tracer=#{@tracer.inspect}>"
131
+ "#<#{LLM::Utils.object_id(self)} @provider=#{@llm.class} @tracer=#{@tracer.inspect}>"
132
132
  end
133
133
 
134
134
  ##
@@ -83,7 +83,7 @@ class LLM::Transport
83
83
  ##
84
84
  # @return [String]
85
85
  def inspect
86
- "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
86
+ "#<#{LLM::Utils.object_id(self)}>"
87
87
  end
88
88
 
89
89
  private
@@ -13,13 +13,15 @@ class LLM::Transport
13
13
  attr_reader :parser
14
14
 
15
15
  ##
16
- # @param [#parse!, #body] parser
16
+ # @param [#parse!, #body, nil] parser
17
+ # @yieldparam [Hash] chunk
17
18
  # @return [LLM::Transport::StreamDecoder]
18
- def initialize(parser)
19
+ def initialize(parser = nil, &on_chunk)
19
20
  @buffer = +""
20
21
  @cursor = 0
21
22
  @data = []
22
23
  @parser = parser
24
+ @on_chunk = on_chunk
23
25
  end
24
26
 
25
27
  ##
@@ -78,7 +80,8 @@ class LLM::Transport
78
80
  def decode!(payload)
79
81
  return if payload.empty? || payload == "[DONE]"
80
82
  chunk = LLM.json.load(payload)
81
- parser.parse!(chunk) if chunk
83
+ return unless chunk
84
+ parser ? parser.parse!(chunk) : @on_chunk&.call(chunk)
82
85
  rescue *LLM.json.parser_error
83
86
  end
84
87