ruby_agent 0.2.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '039e385b0c47944839ae33f1b8e4ed75e499803ae06538e9da4c3a9899dbe247'
4
+ data.tar.gz: 6726c84e2a9fbb9f795300dbb8995fba831bb989540681ce35c3b6604c9c6b35
5
+ SHA512:
6
+ metadata.gz: 787bdc5703131d4169ca2fb53175ce3cc920330fd398e34c881ab2fbf4bfdf73a360a3459a60891fbdd05bb4891a7ff41f74efca715ef126164e63a128daf5ef
7
+ data.tar.gz: 0c273eb33000ddc93e065a3b1d43483012012fa350e67bcae2f96def1d882d3f7e2846659fbca4287b5b0e280201023c0469cccfeec72b8282a0d218d831854c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Keith Schacht
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # RubyAgent
2
+
3
+ A Ruby framework for building AI agents powered by Claude Code. This gem provides a simple, event-driven interface to interact with Claude through the Claude Code CLI.
4
+
5
+ ## Prerequisites
6
+
7
+ Before using RubyAgent, you need to install Claude Code CLI:
8
+
9
+ ### macOS
10
+ ```bash
11
+ curl -fsSL https://claude.ai/install.sh | bash
12
+ ```
13
+
14
+ ### Windows
15
+ ```powershell
16
+ irm https://claude.ai/install.ps1 | iex
17
+ ```
18
+
19
+ For more information, visit the [Claude Code documentation](https://www.claude.com/product/claude-code).
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ gem install ruby_agent
25
+ ```
26
+
27
+ Or add to your Gemfile:
28
+
29
+ ```ruby
30
+ gem 'ruby_agent'
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Basic Example
36
+
37
+ ```ruby
38
+ require 'ruby_agent'
39
+
40
+ agent = RubyAgent.new(
41
+ system_prompt: "You are a helpful assistant",
42
+ model: "claude-sonnet-4-5-20250929"
43
+ )
44
+
45
+ agent.on_assistant do |event, all_events|
46
+ if event.dig("message", "content", 0, "type") == "text"
47
+ text = event.dig("message", "content", 0, "text")
48
+ puts "Assistant: #{text}"
49
+ end
50
+ end
51
+
52
+ agent.on_result do |event, all_events|
53
+ puts "Result: #{event['subtype']}"
54
+ agent.exit if event["subtype"] == "success"
55
+ end
56
+
57
+ agent.connect do
58
+ agent.ask("What is 1+1?", sender_name: "User")
59
+ end
60
+ ```
61
+
62
+ ### Advanced Example with Callbacks
63
+
64
+ ```ruby
65
+ require 'ruby_agent'
66
+
67
+ agent = RubyAgent.new(
68
+ sandbox_dir: Dir.pwd,
69
+ system_prompt: "You are a helpful coding assistant",
70
+ model: "claude-sonnet-4-5-20250929",
71
+ verbose: true
72
+ )
73
+
74
+ agent.create_message_callback :assistant_text do |event, all_events|
75
+ if event["type"] == "assistant" && event.dig("message", "content", 0, "type") == "text"
76
+ event.dig("message", "content", 0, "text")
77
+ end
78
+ end
79
+
80
+ agent.on_system_init do |event, _|
81
+ puts "Session started: #{event['session_id']}"
82
+ end
83
+
84
+ agent.on_assistant_text do |text|
85
+ puts "Claude says: #{text}"
86
+ end
87
+
88
+ agent.on_result do |event, all_events|
89
+ if event["subtype"] == "success"
90
+ puts "Task completed successfully!"
91
+ agent.exit
92
+ elsif event["subtype"] == "error_occurred"
93
+ puts "Error: #{event['result']}"
94
+ agent.exit
95
+ end
96
+ end
97
+
98
+ agent.on_error do |error|
99
+ puts "Error occurred: #{error.message}"
100
+ end
101
+
102
+ agent.connect do
103
+ agent.ask("Write a simple Hello World function in Ruby", sender_name: "User")
104
+ end
105
+ ```
106
+
107
+ ### Resuming Sessions
108
+
109
+ ```ruby
110
+ agent = RubyAgent.new(
111
+ session_key: "existing_session_123",
112
+ system_prompt: "You are a helpful assistant"
113
+ )
114
+
115
+ agent.connect do
116
+ agent.ask("Continue from where we left off", sender_name: "User")
117
+ end
118
+ ```
119
+
120
+ ### Using ERB in System Prompts
121
+
122
+ ```ruby
123
+ agent = RubyAgent.new(
124
+ system_prompt: "You are <%= role %> working on <%= project_name %>",
125
+ role: "a senior developer",
126
+ project_name: "RubyAgent"
127
+ )
128
+ ```
129
+
130
+ ## Event Callbacks
131
+
132
+ RubyAgent supports dynamic event callbacks using `method_missing`. You can create callbacks for any event type:
133
+
134
+ - `on_message` (alias: `on_event`) - Triggered for every message
135
+ - `on_assistant` - Triggered when Claude responds
136
+ - `on_system_init` - Triggered when a session starts
137
+ - `on_result` - Triggered when a task completes
138
+ - `on_error` - Triggered when an error occurs
139
+ - `on_tool_use` - Triggered when Claude uses a tool
140
+ - `on_tool_result` - Triggered when a tool returns results
141
+
142
+ You can also create custom callbacks with specific subtypes like `on_system_init`, `on_error_timeout`, etc.
143
+
144
+ ## Custom Message Callbacks
145
+
146
+ Create custom message processors that filter and transform events:
147
+
148
+ ```ruby
149
+ agent.create_message_callback :important_messages do |message, all_messages|
150
+ if message["type"] == "assistant"
151
+ message.dig("message", "content", 0, "text")
152
+ end
153
+ end
154
+
155
+ agent.on_important_messages do |text|
156
+ puts "Important: #{text}"
157
+ end
158
+ ```
159
+
160
+ ## API
161
+
162
+ ### RubyAgent.new(options)
163
+
164
+ Creates a new RubyAgent instance.
165
+
166
+ **Options:**
167
+ - `sandbox_dir` (String) - Working directory for the agent (default: `Dir.pwd`)
168
+ - `timezone` (String) - Timezone for the agent (default: `"UTC"`)
169
+ - `skip_permissions` (Boolean) - Skip permission prompts (default: `true`)
170
+ - `verbose` (Boolean) - Enable verbose output (default: `false`)
171
+ - `system_prompt` (String) - System prompt for Claude (default: `"You are a helpful assistant"`)
172
+ - `model` (String) - Claude model to use (default: `"claude-sonnet-4-5-20250929"`)
173
+ - `mcp_servers` (Hash) - MCP server configuration (default: `nil`)
174
+ - `session_key` (String) - Resume an existing session (default: `nil`)
175
+ - Additional keyword arguments are passed to the ERB template in `system_prompt`
176
+
177
+ ### Instance Methods
178
+
179
+ - `connect(&block)` - Connect to Claude and execute the block
180
+ - `ask(text, sender_name: "User", additional: [])` - Send a message to Claude
181
+ - `send_system_message(text)` - Send a system message
182
+ - `interrupt` - Interrupt Claude's current operation
183
+ - `exit` - Close the connection to Claude
184
+ - `on_message(&block)` - Register a callback for all messages
185
+ - `on_error(&block)` - Register a callback for errors
186
+ - Dynamic `on_*` methods for specific event types
187
+
188
+ ## Error Handling
189
+
190
+ RubyAgent defines three error types:
191
+
192
+ - `RubyAgent::AgentError` - Base error class
193
+ - `RubyAgent::ConnectionError` - Connection-related errors
194
+ - `RubyAgent::ParseError` - System prompt parsing errors
195
+
196
+ ```ruby
197
+ begin
198
+ agent.connect do
199
+ agent.ask("Hello", sender_name: "User")
200
+ end
201
+ rescue RubyAgent::ConnectionError => e
202
+ puts "Connection failed: #{e.message}"
203
+ rescue RubyAgent::AgentError => e
204
+ puts "Agent error: #{e.message}"
205
+ end
206
+ ```
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,3 @@
1
+ class RubyAgent
2
+ VERSION = "0.2.1"
3
+ end
data/lib/ruby_agent.rb ADDED
@@ -0,0 +1,427 @@
1
+ require_relative "ruby_agent/version"
2
+ require "shellwords"
3
+ require "open3"
4
+ require "erb"
5
+ require "json"
6
+
7
+ class RubyAgent
8
+ class AgentError < StandardError; end
9
+ class ConnectionError < AgentError; end
10
+ class ParseError < AgentError; end
11
+
12
+ DEBUG = false
13
+
14
+ attr_reader :sandbox_dir, :timezone, :skip_permissions, :verbose, :system_prompt, :model, :mcp_servers
15
+
16
+ def initialize(
17
+ sandbox_dir: Dir.pwd,
18
+ timezone: "UTC",
19
+ skip_permissions: true,
20
+ verbose: false,
21
+ system_prompt: "You are a helpful assistant",
22
+ model: "claude-sonnet-4-5-20250929",
23
+ mcp_servers: nil,
24
+ session_key: nil,
25
+ **additional_context
26
+ )
27
+ @sandbox_dir = sandbox_dir
28
+ @timezone = timezone
29
+ @skip_permissions = skip_permissions
30
+ @verbose = verbose
31
+ @model = model
32
+ @mcp_servers = mcp_servers
33
+ @session_key = session_key
34
+ @system_prompt = parse_system_prompt(system_prompt, additional_context)
35
+ @on_message_callback = nil
36
+ @on_error_callback = nil
37
+ @dynamic_callbacks = {}
38
+ @custom_message_callbacks = {}
39
+ @stdin = nil
40
+ @stdout = nil
41
+ @stderr = nil
42
+ @wait_thr = nil
43
+ @parsed_lines = []
44
+ @parsed_lines_mutex = Mutex.new
45
+ @pending_ask_after_interrupt = nil
46
+ @pending_interrupt_request_id = nil
47
+ @deferred_exit = false
48
+
49
+ unless @session_key
50
+ inject_streaming_response({
51
+ type: "system",
52
+ subtype: "prompt",
53
+ system_prompt: @system_prompt,
54
+ timestamp: Time.now.utc.iso8601(6),
55
+ received_at: Time.now.utc.iso8601(6)
56
+ })
57
+ end
58
+ end
59
+
60
+ def create_message_callback(name, &processor)
61
+ @custom_message_callbacks[name.to_s] = {
62
+ processor: processor,
63
+ callback: nil
64
+ }
65
+ end
66
+
67
+ def on_message(&block)
68
+ @on_message_callback = block
69
+ end
70
+
71
+ alias_method :on_event, :on_message
72
+
73
+ def on_error(&block)
74
+ @on_error_callback = block
75
+ end
76
+
77
+ def method_missing(method_name, *args, &block)
78
+ if method_name.to_s.start_with?("on_") && block_given?
79
+ callback_name = method_name.to_s.sub(/^on_/, "")
80
+
81
+ if @custom_message_callbacks.key?(callback_name)
82
+ @custom_message_callbacks[callback_name][:callback] = block
83
+ else
84
+ @dynamic_callbacks[callback_name] = block
85
+ end
86
+ else
87
+ super
88
+ end
89
+ end
90
+
91
+ def respond_to_missing?(method_name, include_private = false)
92
+ method_name.to_s.start_with?("on_") || super
93
+ end
94
+
95
+ def connect(&block)
96
+ command = build_claude_command
97
+
98
+ spawn_process(command, @sandbox_dir) do |stdin, stdout, stderr, wait_thr|
99
+ @stdin = stdin
100
+ @stdout = stdout
101
+ @stderr = stderr
102
+ @wait_thr = wait_thr
103
+
104
+ begin
105
+ block.call if block_given?
106
+ receive_streaming_responses
107
+ ensure
108
+ @stdin = nil
109
+ @stdout = nil
110
+ @stderr = nil
111
+ @wait_thr = nil
112
+ end
113
+ end
114
+ rescue => e
115
+ trigger_error(e)
116
+ raise
117
+ end
118
+
119
+ def ask(text, sender_name: "User", additional: [])
120
+ formatted_text = if sender_name.downcase == "system"
121
+ <<~TEXT.strip
122
+ <system>
123
+ #{text}
124
+ </system>
125
+ TEXT
126
+ else
127
+ "#{sender_name}: #{text}"
128
+ end
129
+ formatted_text += extra_context(additional, sender_name:)
130
+
131
+ inject_streaming_response({
132
+ type: "user",
133
+ subtype: "new_message",
134
+ sender_name:,
135
+ text:,
136
+ formatted_text:,
137
+ timestamp: Time.now.utc.iso8601(6)
138
+ })
139
+
140
+ send_message(formatted_text)
141
+ end
142
+
143
+ def ask_after_interrupt(text, sender_name: "User", additional: [])
144
+ @pending_ask_after_interrupt = {text:, sender_name:, additional:}
145
+ end
146
+
147
+ def send_system_message(text)
148
+ ask(text, sender_name: "system")
149
+ end
150
+
151
+ def receive_streaming_responses
152
+ @stdout.each_line do |line|
153
+ next if line.strip.empty?
154
+
155
+ begin
156
+ json = JSON.parse(line)
157
+
158
+ all_lines = nil
159
+ @parsed_lines_mutex.synchronize do
160
+ @parsed_lines << json
161
+ all_lines = @parsed_lines.dup
162
+ end
163
+
164
+ trigger_message(json, all_lines)
165
+ trigger_dynamic_callbacks(json, all_lines)
166
+ trigger_custom_message_callbacks(json, all_lines)
167
+ rescue JSON::ParserError
168
+ warn "Failed to parse line: #{line}" if DEBUG
169
+ end
170
+ end
171
+
172
+ puts "→ stdout closed, waiting for process to exit..." if DEBUG
173
+ exit_status = @wait_thr.value
174
+ puts "→ Process exited with status: #{exit_status.success? ? "success" : "failure"}" if DEBUG
175
+ unless exit_status.success?
176
+ stderr_output = @stderr.read
177
+ raise ConnectionError, "Claude command failed: #{stderr_output}"
178
+ end
179
+
180
+ @parsed_lines
181
+ end
182
+
183
+ def inject_streaming_response(event_hash)
184
+ stringified_event = stringify_keys(event_hash)
185
+ all_lines = nil
186
+ @parsed_lines_mutex.synchronize do
187
+ @parsed_lines << stringified_event
188
+ all_lines = @parsed_lines.dup
189
+ end
190
+
191
+ trigger_message(stringified_event, all_lines)
192
+ trigger_dynamic_callbacks(stringified_event, all_lines)
193
+ trigger_custom_message_callbacks(stringified_event, all_lines)
194
+ end
195
+
196
+ def interrupt
197
+ raise ConnectionError, "Not connected to Claude" unless @stdin
198
+ raise ConnectionError, "Cannot interrupt - stdin is closed" if @stdin.closed?
199
+
200
+ @request_counter ||= 0
201
+ @request_counter += 1
202
+ request_id = "req_#{@request_counter}_#{SecureRandom.hex(4)}"
203
+
204
+ @pending_interrupt_request_id = request_id if @pending_ask_after_interrupt
205
+ puts "→ Sending interrupt with request_id: #{request_id}, pending_ask: #{@pending_ask_after_interrupt ? true : false}" if DEBUG
206
+
207
+ control_request = {
208
+ type: "control_request",
209
+ request_id: request_id,
210
+ request: {
211
+ subtype: "interrupt"
212
+ }
213
+ }
214
+
215
+ inject_streaming_response({
216
+ type: "control",
217
+ subtype: "interrupt",
218
+ timestamp: Time.now.utc.iso8601(6)
219
+ })
220
+
221
+ @stdin.puts JSON.generate(control_request)
222
+ @stdin.flush
223
+ rescue => e
224
+ warn "Failed to send interrupt signal: #{e.message}"
225
+ raise
226
+ end
227
+
228
+ def exit
229
+ return unless @stdin
230
+
231
+ if @pending_interrupt_request_id
232
+ puts "→ Deferring exit - waiting for interrupt response (request_id: #{@pending_interrupt_request_id})" if DEBUG
233
+ @deferred_exit = true
234
+ return
235
+ end
236
+
237
+ puts "→ Exiting Claude (closing stdin)" if DEBUG
238
+
239
+ begin
240
+ @stdin.close unless @stdin.closed?
241
+ puts "→ stdin closed" if DEBUG
242
+ rescue => e
243
+ warn "Error closing stdin during exit: #{e.message}"
244
+ end
245
+ end
246
+
247
+ private
248
+
249
+ def spawn_process(command, sandbox_dir, &)
250
+ Open3.popen3("bash", "-lc", command, chdir: sandbox_dir, &)
251
+ end
252
+
253
+ def build_claude_command
254
+ cmd = "claude -p --dangerously-skip-permissions --output-format=stream-json --input-format=stream-json --verbose"
255
+ cmd += " --system-prompt #{Shellwords.escape(@system_prompt)}"
256
+ cmd += " --model #{Shellwords.escape(@model)}"
257
+
258
+ if @mcp_servers
259
+ mcp_config = build_mcp_config(@mcp_servers)
260
+ cmd += " --mcp-config #{Shellwords.escape(mcp_config.to_json)}"
261
+ end
262
+
263
+ cmd += " --setting-sources \"\""
264
+ cmd += " --resume #{Shellwords.escape(@session_key)}" if @session_key
265
+ cmd
266
+ end
267
+
268
+ def build_mcp_config(mcp_servers)
269
+ servers = mcp_servers.transform_keys { |k| k.to_s.gsub("_", "-") }
270
+ {mcpServers: servers}
271
+ end
272
+
273
+ def parse_system_prompt(template_content, context_vars)
274
+ if Dir.exist?(@sandbox_dir)
275
+ Dir.chdir(@sandbox_dir) do
276
+ parse_system_prompt_in_context(template_content, context_vars)
277
+ end
278
+ else
279
+ parse_system_prompt_in_context(template_content, context_vars)
280
+ end
281
+ end
282
+
283
+ def parse_system_prompt_in_context(template_content, context_vars)
284
+ erb = ERB.new(template_content)
285
+ binding_context = create_binding_context(**context_vars)
286
+ result = erb.result(binding_context)
287
+
288
+ if result.include?("<%=") || result.include?("%>")
289
+ raise ParseError, "There was an error parsing the system prompt."
290
+ end
291
+
292
+ result
293
+ end
294
+
295
+ def create_binding_context(**vars)
296
+ context = Object.new
297
+ vars.each do |key, value|
298
+ context.instance_variable_set("@#{key}", value)
299
+ context.define_singleton_method(key) { instance_variable_get("@#{key}") }
300
+ end
301
+ context.instance_eval { binding }
302
+ end
303
+
304
+ def extra_context(additional = [], sender_name:)
305
+ raise "additional is not an array" unless additional.is_a?(Array)
306
+
307
+ return "" if additional.empty?
308
+
309
+ <<~CONTEXT
310
+
311
+ <extra-context>
312
+ #{additional.join("\n\n")}
313
+ </extra-context>
314
+ CONTEXT
315
+ end
316
+
317
+ def send_message(content, session_id = nil)
318
+ raise ConnectionError, "Not connected to Claude" unless @stdin
319
+
320
+ message_json = {
321
+ type: "user",
322
+ message: {role: "user", content: content},
323
+ session_id: session_id
324
+ }.compact
325
+
326
+ @stdin.puts JSON.generate(message_json)
327
+ @stdin.flush
328
+ rescue => e
329
+ trigger_error(e)
330
+ raise
331
+ end
332
+
333
+ def trigger_message(message, all_messages)
334
+ @on_message_callback&.call(message, all_messages)
335
+ end
336
+
337
+ def trigger_dynamic_callbacks(message, all_messages)
338
+ type = message["type"]
339
+ subtype = message["subtype"]
340
+
341
+ return unless type
342
+
343
+ if type == "control_response"
344
+ puts "→ Received control_response: #{message.inspect}" if DEBUG || @pending_interrupt_request_id
345
+ if @pending_interrupt_request_id
346
+ response = message["response"]
347
+ if response&.dig("subtype") == "success" && response&.dig("request_id") == @pending_interrupt_request_id
348
+ puts "→ Interrupt confirmed, executing queued ask" if DEBUG
349
+ @pending_interrupt_request_id = nil
350
+ if @pending_ask_after_interrupt
351
+ pending = @pending_ask_after_interrupt
352
+ @pending_ask_after_interrupt = nil
353
+ begin
354
+ ask(pending[:text], sender_name: pending[:sender_name], additional: pending[:additional])
355
+ rescue IOError, Errno::EPIPE => e
356
+ warn "Failed to send queued ask after interrupt (stream closed): #{e.message}"
357
+ end
358
+ end
359
+
360
+ if @deferred_exit
361
+ puts "→ Executing deferred exit" if DEBUG
362
+ @deferred_exit = false
363
+ exit
364
+ end
365
+ elsif DEBUG
366
+ puts "→ Control response didn't match pending interrupt: #{response.inspect}"
367
+ end
368
+ end
369
+ end
370
+
371
+ if subtype
372
+ specific_callback_key = "#{type}_#{subtype}"
373
+ specific_callback = @dynamic_callbacks[specific_callback_key]
374
+ if specific_callback
375
+ puts "→ Triggering callback for: #{specific_callback_key}" if DEBUG
376
+ specific_callback.call(message, all_messages)
377
+ end
378
+ end
379
+
380
+ general_callback = @dynamic_callbacks[type]
381
+ if general_callback
382
+ puts "→ Triggering callback for: #{type}" if DEBUG
383
+ general_callback.call(message, all_messages)
384
+ end
385
+
386
+ check_nested_content_types(message, all_messages)
387
+ end
388
+
389
+ def check_nested_content_types(message, all_messages)
390
+ return unless message["message"].is_a?(Hash)
391
+ content = message.dig("message", "content")
392
+ return unless content.is_a?(Array)
393
+
394
+ content.each do |content_item|
395
+ next unless content_item.is_a?(Hash)
396
+
397
+ nested_type = content_item["type"]
398
+ next unless nested_type
399
+
400
+ callback = @dynamic_callbacks[nested_type]
401
+ if callback
402
+ puts "→ Triggering callback for nested type: #{nested_type}" if DEBUG
403
+ callback.call(message, all_messages)
404
+ end
405
+ end
406
+ end
407
+
408
+ def trigger_custom_message_callbacks(message, all_messages)
409
+ @custom_message_callbacks.each do |name, config|
410
+ processor = config[:processor]
411
+ callback = config[:callback]
412
+
413
+ next unless processor && callback
414
+
415
+ result = processor.call(message, all_messages)
416
+ callback.call(result) if result && !result.to_s.empty?
417
+ end
418
+ end
419
+
420
+ def trigger_error(error)
421
+ @on_error_callback&.call(error)
422
+ end
423
+
424
+ def stringify_keys(hash)
425
+ hash.transform_keys(&:to_s)
426
+ end
427
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Keith Schacht
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A framework for building AI agents in Ruby
13
+ email:
14
+ - keith@keithschacht.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/ruby_agent.rb
22
+ - lib/ruby_agent/version.rb
23
+ homepage: https://github.com/keithschacht/ruby-agent
24
+ licenses:
25
+ - MIT
26
+ metadata:
27
+ homepage_uri: https://github.com/keithschacht/ruby-agent
28
+ source_code_uri: https://github.com/keithschacht/ruby-agent
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.7.0
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.6.7
44
+ specification_version: 4
45
+ summary: Ruby agent framework
46
+ test_files: []