agentdyne 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1c1119bd1943f2861ad02a8e489c4c3e6185712ff487d8800d1f8ce1133cb262
4
+ data.tar.gz: 01be54ee75e15986f9d3a830570d653794102ee88286c030ba2bbf4f77c83e74
5
+ SHA512:
6
+ metadata.gz: '091bdb780213fa5b05438fa49a58690f5f3cc623959c548a54da0b52de7a9fae66bb014d7ac3b9fa16f9742c809e920cf72a7a4bb73f48a9770a8d901300fb75'
7
+ data.tar.gz: 9c89bf8c3d2b0843c90152536d61d17dfbcf96e1798180d3aee5627791d8de84176e2a7d53c51175181a280eb91e028ec3c23c5e348005ca39a9bb76a0fc3acc
data/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # agentdyne
2
+
3
+ Official Ruby SDK for [AgentDyne](https://agentdyne.com) — The Global Microagent Marketplace.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/agentdyne.svg)](https://rubygems.org/gems/agentdyne)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ ## Installation
9
+
10
+ ```ruby
11
+ # Gemfile
12
+ gem "agentdyne"
13
+ ```
14
+
15
+ ```bash
16
+ bundle install
17
+ # or standalone:
18
+ gem install agentdyne
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```ruby
24
+ require "agentdyne"
25
+
26
+ client = AgentDyne.new(api_key: "agd_your_key_here")
27
+
28
+ result = client.execute("agent_id", "Summarize this email thread...")
29
+ puts result.output
30
+ # => { "summary" => "...", "action_items" => [...] }
31
+
32
+ puts "Latency: #{result.latency_ms}ms Cost: $#{"%.6f" % result.cost}"
33
+ ```
34
+
35
+ ## Authentication
36
+
37
+ ```bash
38
+ export AGENTDYNE_API_KEY=agd_your_key_here
39
+ ```
40
+
41
+ ```ruby
42
+ # Reads from env automatically
43
+ client = AgentDyne.new
44
+
45
+ # Or pass directly
46
+ client = AgentDyne.new(api_key: "agd_...")
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### List Agents
52
+
53
+ ```ruby
54
+ page = client.list_agents(category: "coding", sort: "rating", limit: 10)
55
+ page.data.each do |agent|
56
+ puts "#{agent.name} ★#{agent.average_rating} #{agent.pricing_model}"
57
+ end
58
+ puts "Total: #{page.pagination.total}"
59
+ ```
60
+
61
+ ### Execute an Agent
62
+
63
+ ```ruby
64
+ # String input
65
+ result = client.execute("email-summarizer-pro", "Hi team, the Q4 report is attached...")
66
+
67
+ # Structured input
68
+ result = client.execute("code-review-agent", {
69
+ code: "def add(a, b)\n a + b\nend",
70
+ language: "ruby"
71
+ })
72
+
73
+ # With idempotency key (safe to retry on network failure)
74
+ result = client.execute("agent_id", "Hello", idempotency_key: SecureRandom.uuid)
75
+
76
+ puts result.output
77
+ puts "#{result.tokens.input} in / #{result.tokens.output} out tokens"
78
+ ```
79
+
80
+ ### Streaming
81
+
82
+ ```ruby
83
+ client.stream("content-writer", "Write a blog post about AI agents in 2026") do |chunk|
84
+ case chunk.type
85
+ when "delta"
86
+ print chunk.delta
87
+ $stdout.flush
88
+ when "done"
89
+ puts "\n✓ Done (execution: #{chunk.execution_id})"
90
+ when "error"
91
+ warn "Stream error: #{chunk.error}"
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Poll Execution
97
+
98
+ ```ruby
99
+ # Start an execution (imagine it was async)
100
+ exec = client.poll_execution("exec_id", interval: 0.5, timeout: 60)
101
+ puts exec.status # => "success"
102
+ puts exec.output
103
+ ```
104
+
105
+ ### Paginate All Agents
106
+
107
+ ```ruby
108
+ # Automatically pages through all results
109
+ client.paginate_agents(category: "finance").each do |agent|
110
+ puts agent.name
111
+ end
112
+ ```
113
+
114
+ ### User & Quota
115
+
116
+ ```ruby
117
+ me = client.me
118
+ puts me.subscription_plan # => "pro"
119
+ puts me.full_name
120
+
121
+ quota = client.my_quota
122
+ puts "#{quota.used}/#{quota.quota} calls used (#{quota.percent_used}%)"
123
+ puts "Resets: #{quota.resets_at}"
124
+
125
+ # Update profile
126
+ client.update_profile(full_name: "Ada Lovelace", bio: "Building AI agents")
127
+ ```
128
+
129
+ ### Reviews
130
+
131
+ ```ruby
132
+ # List reviews
133
+ page = client.list_reviews("agent_id")
134
+ page.data.each { |r| puts "★#{r.rating} — #{r.title}" }
135
+
136
+ # Post a review (must have executed the agent first)
137
+ review = client.create_review("agent_id",
138
+ rating: 5,
139
+ title: "Incredible accuracy",
140
+ body: "Handles every edge case I've thrown at it."
141
+ )
142
+ ```
143
+
144
+ ### Webhooks
145
+
146
+ ```ruby
147
+ # Rails controller
148
+ class WebhooksController < ApplicationController
149
+ skip_before_action :verify_authenticity_token
150
+
151
+ def agentdyne
152
+ client = AgentDyne.new
153
+ event = client.construct_webhook_event(
154
+ request.raw_post,
155
+ request.headers["X-AgentDyne-Signature"],
156
+ ENV["AGENTDYNE_WEBHOOK_SECRET"]
157
+ )
158
+
159
+ case event.type
160
+ when "execution.completed"
161
+ Rails.logger.info "Execution done: #{event.data["executionId"]}"
162
+ when "payout.processed"
163
+ Payout.record!(event.data)
164
+ end
165
+
166
+ head :ok
167
+
168
+ rescue AgentDyne::WebhookSignatureError
169
+ head :bad_request
170
+ end
171
+ end
172
+ ```
173
+
174
+ ```ruby
175
+ # Sinatra
176
+ post "/webhook" do
177
+ client = AgentDyne.new
178
+ event = client.construct_webhook_event(
179
+ request.body.read,
180
+ request.env["HTTP_X_AGENTDYNE_SIGNATURE"],
181
+ ENV["AGENTDYNE_WEBHOOK_SECRET"]
182
+ )
183
+ "OK"
184
+ rescue AgentDyne::WebhookSignatureError
185
+ [400, "Invalid signature"]
186
+ end
187
+ ```
188
+
189
+ ## Error Handling
190
+
191
+ Every error inherits from `AgentDyne::AgentDyneError`:
192
+
193
+ ```ruby
194
+ require "agentdyne"
195
+
196
+ begin
197
+ result = client.execute("agent_id", "Hello")
198
+ rescue AgentDyne::QuotaExceededError => e
199
+ puts "Upgrade at agentdyne.com/billing (plan: #{e.plan})"
200
+ rescue AgentDyne::RateLimitError => e
201
+ sleep e.retry_after_seconds
202
+ retry
203
+ rescue AgentDyne::SubscriptionRequiredError => e
204
+ puts "Subscribe to use agent: #{e.agent_id}"
205
+ rescue AgentDyne::NotFoundError
206
+ puts "Agent not found"
207
+ rescue AgentDyne::AuthenticationError
208
+ puts "Check your API key"
209
+ rescue AgentDyne::AgentDyneError => e
210
+ puts "#{e.message} (HTTP #{e.status_code}, code=#{e.code})"
211
+ end
212
+ ```
213
+
214
+ ## Configuration
215
+
216
+ ```ruby
217
+ client = AgentDyne.new(
218
+ api_key: "agd_...",
219
+ base_url: "http://localhost:3000", # local dev override
220
+ max_retries: 3, # retries on 429/5xx
221
+ timeout: 60 # seconds
222
+ )
223
+ ```
224
+
225
+ ## Requirements
226
+
227
+ - Ruby >= 3.1
228
+ - No required dependencies (uses stdlib `net/http`, `openssl`, `json`)
229
+ - Optional: `rack` gem for `secure_compare` in webhook verification
230
+
231
+ ## License
232
+
233
+ MIT © 2026 AgentDyne, Inc.
data/agentdyne.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "agentdyne"
3
+ spec.version = "1.0.0"
4
+ spec.authors = ["AgentDyne, Inc."]
5
+ spec.email = ["sdk@agentdyne.com"]
6
+ spec.summary = "Official Ruby SDK for AgentDyne — The Global Microagent Marketplace"
7
+ spec.description = "Discover, execute, and monetise AI agents with the AgentDyne Ruby SDK. Zero required dependencies."
8
+ spec.homepage = "https://agentdyne.com"
9
+ spec.license = "MIT"
10
+
11
+ spec.metadata = {
12
+ "homepage_uri" => "https://agentdyne.com",
13
+ "source_code_uri" => "https://github.com/agentdyne/sdk-ruby",
14
+ "bug_tracker_uri" => "https://github.com/agentdyne/sdk-ruby/issues",
15
+ "documentation_uri" => "https://agentdyne.com/docs",
16
+ }
17
+
18
+ spec.required_ruby_version = ">= 3.1"
19
+ spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "agentdyne.gemspec"]
20
+ spec.require_paths = ["lib"]
21
+
22
+ # Zero required dependencies — uses Ruby's stdlib (net/http, openssl, json).
23
+ # Optional: rack for webhook secure_compare
24
+ spec.add_runtime_dependency "rack", ">= 2.0"
25
+
26
+ spec.add_development_dependency "rspec", "~> 3.13"
27
+ spec.add_development_dependency "webmock", "~> 3.23"
28
+ spec.add_development_dependency "rake", "~> 13.2"
29
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+ require "ostruct"
6
+
7
+ module AgentDyne
8
+ # The main AgentDyne Ruby client.
9
+ #
10
+ # @example
11
+ # client = AgentDyne::Client.new(api_key: "agd_...")
12
+ # result = client.execute("agent_id", "Summarize this...")
13
+ # puts result.output
14
+ #
15
+ class Client
16
+ TERMINAL_STATUSES = %w[success failed timeout].freeze
17
+
18
+ # @param api_key [String] Your AgentDyne API key. Falls back to
19
+ # the AGENTDYNE_API_KEY environment variable.
20
+ # @param base_url [String] API base URL (default: https://api.agentdyne.com).
21
+ # @param max_retries [Integer] Retries on 429/5xx (default: 3).
22
+ # @param timeout [Integer] Request timeout in seconds (default: 60).
23
+ def initialize(
24
+ api_key: nil,
25
+ base_url: "https://api.agentdyne.com",
26
+ max_retries: 3,
27
+ timeout: 60
28
+ )
29
+ resolved_key = api_key || ENV["AGENTDYNE_API_KEY"]
30
+ raise ArgumentError, "AgentDyne API key is required. Pass api_key: or set AGENTDYNE_API_KEY." if resolved_key.nil? || resolved_key.empty?
31
+
32
+ @http = HttpClient.new(
33
+ api_key: resolved_key,
34
+ base_url: base_url,
35
+ timeout: timeout,
36
+ max_retries: max_retries,
37
+ )
38
+ end
39
+
40
+ # ── Agents ──────────────────────────────────────────────────────────────
41
+
42
+ # List agents with optional filters.
43
+ #
44
+ # @param q [String, nil] Full-text search query.
45
+ # @param category [String, nil] Agent category.
46
+ # @param pricing [String, nil] Pricing model filter.
47
+ # @param sort [String, nil] Sort order ("popular", "rating", "newest").
48
+ # @param page [Integer] Page number (default: 1).
49
+ # @param limit [Integer] Results per page (default: 24, max: 100).
50
+ # @return [OpenStruct] with .data (Array) and .pagination.
51
+ #
52
+ # @example
53
+ # page = client.list_agents(category: "coding", sort: "rating")
54
+ # page.data.each { |a| puts "#{a['name']} ★#{a['average_rating']}" }
55
+ def list_agents(q: nil, category: nil, pricing: nil, sort: nil, page: 1, limit: 24)
56
+ params = { page: page, limit: limit }
57
+ params[:q] = q if q
58
+ params[:category] = category if category
59
+ params[:pricing] = pricing if pricing
60
+ params[:sort] = sort if sort
61
+ raw = @http.get("/v1/agents", params)
62
+ to_ostruct(raw)
63
+ end
64
+
65
+ # Get a single agent by ID.
66
+ #
67
+ # @return [OpenStruct]
68
+ def get_agent(agent_id)
69
+ to_ostruct(@http.get("/v1/agents/#{agent_id}"))
70
+ end
71
+
72
+ # Search agents by keyword.
73
+ def search_agents(query, **kwargs)
74
+ list_agents(q: query, **kwargs)
75
+ end
76
+
77
+ # Iterate through ALL matching agents across pages automatically.
78
+ #
79
+ # @example
80
+ # client.paginate_agents(category: "finance").each { |a| puts a.name }
81
+ def paginate_agents(**kwargs)
82
+ Enumerator.new do |y|
83
+ p = 1
84
+ loop do
85
+ page = list_agents(page: p, **kwargs)
86
+ page.data.each { |item| y << to_ostruct(item) }
87
+ break unless page.pagination.has_next
88
+ p += 1
89
+ end
90
+ end
91
+ end
92
+
93
+ # ── Execution ────────────────────────────────────────────────────────────
94
+
95
+ # Execute an agent synchronously and return the output.
96
+ #
97
+ # @param agent_id [String]
98
+ # @param input [String, Hash, Array] Input to the agent.
99
+ # @param idempotency_key [String, nil] Optional UUID for safe retries.
100
+ # @return [OpenStruct] with .output, .latency_ms, .cost, .tokens, .execution_id
101
+ #
102
+ # @example
103
+ # result = client.execute("code-review-agent", { code: "def f(): pass", language: "python" })
104
+ # puts result.output
105
+ def execute(agent_id, input, idempotency_key: nil)
106
+ body = { input: input }
107
+ body[:idempotencyKey] = idempotency_key if idempotency_key
108
+ raw = @http.post("/v1/agents/#{agent_id}/execute", body)
109
+ to_ostruct(raw)
110
+ end
111
+
112
+ # Stream an agent's output.
113
+ # Yields StreamChunk-like OpenStructs with .type, .delta, .execution_id.
114
+ #
115
+ # @example
116
+ # client.stream("content-writer", "Write a haiku about Ruby") do |chunk|
117
+ # print chunk.delta if chunk.type == "delta"
118
+ # end
119
+ def stream(agent_id, input, &block)
120
+ @http.stream("/v1/agents/#{agent_id}/execute", { input: input, stream: true }) do |raw_line|
121
+ begin
122
+ data = JSON.parse(raw_line)
123
+ chunk = OpenStruct.new(
124
+ type: data["type"] || "delta",
125
+ delta: data["delta"],
126
+ execution_id: data["executionId"],
127
+ error: data["error"],
128
+ )
129
+ rescue JSON::ParserError
130
+ chunk = OpenStruct.new(type: "delta", delta: raw_line)
131
+ end
132
+ block.call(chunk)
133
+ return if chunk.type == "done"
134
+ end
135
+ end
136
+
137
+ # ── Executions ───────────────────────────────────────────────────────────
138
+
139
+ # Get a single execution by ID.
140
+ def get_execution(execution_id)
141
+ to_ostruct(@http.get("/v1/executions/#{execution_id}"))
142
+ end
143
+
144
+ # List execution history.
145
+ def list_executions(agent_id: nil, status: nil, page: 1, limit: 20)
146
+ params = { page: page, limit: limit }
147
+ params[:agentId] = agent_id if agent_id
148
+ params[:status] = status if status
149
+ to_ostruct(@http.get("/v1/executions", params))
150
+ end
151
+
152
+ # Poll until an execution reaches a terminal state.
153
+ #
154
+ # @param interval [Float] Seconds between polls (default: 1.0).
155
+ # @param timeout [Float] Maximum seconds to wait (default: 120.0).
156
+ # @raise [AgentDyneError] if the execution does not complete in time.
157
+ #
158
+ # @example
159
+ # exec = client.poll_execution("exec_id", interval: 0.5)
160
+ def poll_execution(execution_id, interval: 1.0, timeout: 120.0)
161
+ deadline = Time.now + timeout
162
+ loop do
163
+ ex = get_execution(execution_id)
164
+ return ex if TERMINAL_STATUSES.include?(ex.status)
165
+ raise AgentDyneError, "Execution \"#{execution_id}\" did not complete within #{timeout}s" if Time.now > deadline
166
+ sleep(interval)
167
+ end
168
+ end
169
+
170
+ # ── User ─────────────────────────────────────────────────────────────────
171
+
172
+ # Return the authenticated user's profile.
173
+ def me
174
+ to_ostruct(@http.get("/v1/user/me"))
175
+ end
176
+
177
+ # Return quota usage for the current billing period.
178
+ def my_quota
179
+ to_ostruct(@http.get("/v1/user/quota"))
180
+ end
181
+
182
+ # Update profile fields.
183
+ def update_profile(**updates)
184
+ to_ostruct(@http.patch("/v1/user/me", updates))
185
+ end
186
+
187
+ # ── Reviews ──────────────────────────────────────────────────────────────
188
+
189
+ # List approved reviews for an agent.
190
+ def list_reviews(agent_id, page: 1, limit: 20)
191
+ to_ostruct(@http.get("/v1/agents/#{agent_id}/reviews", { page: page, limit: limit }))
192
+ end
193
+
194
+ # Post a review for an agent.
195
+ def create_review(agent_id, rating:, title: nil, body: nil)
196
+ payload = { rating: rating }
197
+ payload[:title] = title if title
198
+ payload[:body] = body if body
199
+ to_ostruct(@http.post("/v1/agents/#{agent_id}/reviews", payload))
200
+ end
201
+
202
+ # ── Notifications ────────────────────────────────────────────────────────
203
+
204
+ # List your notifications.
205
+ def list_notifications
206
+ @http.get("/v1/notifications").fetch("notifications", []).map { |n| to_ostruct(n) }
207
+ end
208
+
209
+ # Mark all notifications as read.
210
+ def mark_notifications_read
211
+ @http.patch("/v1/notifications").fetch("ok", false)
212
+ end
213
+
214
+ # ── Webhooks ─────────────────────────────────────────────────────────────
215
+
216
+ # Verify and parse an incoming AgentDyne webhook.
217
+ #
218
+ # Raises WebhookSignatureError if the signature is invalid.
219
+ #
220
+ # @param payload [String] Raw request body.
221
+ # @param signature [String] Value of X-AgentDyne-Signature header.
222
+ # @param secret [String] Your webhook signing secret.
223
+ # @return [OpenStruct] Parsed event with .type and .data.
224
+ #
225
+ # @example (Sinatra)
226
+ # post "/webhook" do
227
+ # event = client.construct_webhook_event(
228
+ # request.body.read,
229
+ # request.env["HTTP_X_AGENTDYNE_SIGNATURE"],
230
+ # ENV["WEBHOOK_SECRET"]
231
+ # )
232
+ # case event.type
233
+ # when "execution.completed" then process_execution(event.data)
234
+ # end
235
+ # "OK"
236
+ # end
237
+ def construct_webhook_event(payload, signature, secret)
238
+ sig_clean = signature.to_s.sub(/^sha256=/, "")
239
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
240
+
241
+ unless Rack::Utils.secure_compare(expected, sig_clean) rescue (expected == sig_clean)
242
+ raise WebhookSignatureError
243
+ end
244
+
245
+ begin
246
+ to_ostruct(JSON.parse(payload))
247
+ rescue JSON::ParserError
248
+ raise WebhookSignatureError, "Webhook payload is not valid JSON"
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ def to_ostruct(obj)
255
+ case obj
256
+ when Hash then OpenStruct.new(obj.transform_values { |v| to_ostruct(v) })
257
+ when Array then obj.map { |item| to_ostruct(item) }
258
+ else obj
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentDyne
4
+ # Base error for all AgentDyne SDK exceptions.
5
+ class AgentDyneError < StandardError
6
+ attr_reader :status_code, :code, :raw
7
+
8
+ def initialize(message, status_code: nil, code: nil, raw: nil)
9
+ super(message)
10
+ @status_code = status_code
11
+ @code = code
12
+ @raw = raw
13
+ end
14
+
15
+ def to_h
16
+ { error: self.class.name, message: message, status_code: status_code, code: code }
17
+ end
18
+ end
19
+
20
+ # HTTP 401 — API key missing, invalid, or revoked.
21
+ class AuthenticationError < AgentDyneError
22
+ def initialize(message = "Invalid or missing API key", raw: nil)
23
+ super(message, status_code: 401, code: "AUTHENTICATION_ERROR", raw: raw)
24
+ end
25
+ end
26
+
27
+ # HTTP 403 — insufficient permissions.
28
+ class PermissionDeniedError < AgentDyneError
29
+ def initialize(message = "Permission denied", raw: nil)
30
+ super(message, status_code: 403, code: "PERMISSION_DENIED", raw: raw)
31
+ end
32
+ end
33
+
34
+ # HTTP 403 / SUBSCRIPTION_REQUIRED — agent requires subscription.
35
+ class SubscriptionRequiredError < AgentDyneError
36
+ attr_reader :agent_id
37
+
38
+ def initialize(agent_id = nil, raw: nil)
39
+ msg = agent_id ? "Agent \"#{agent_id}\" requires an active subscription" : "Subscription required"
40
+ super(msg, status_code: 403, code: "SUBSCRIPTION_REQUIRED", raw: raw)
41
+ @agent_id = agent_id
42
+ end
43
+ end
44
+
45
+ # HTTP 404 — resource not found.
46
+ class NotFoundError < AgentDyneError
47
+ def initialize(message = "Resource not found", raw: nil)
48
+ super(message, status_code: 404, code: "NOT_FOUND", raw: raw)
49
+ end
50
+ end
51
+
52
+ # HTTP 400 — malformed request or missing required fields.
53
+ class ValidationError < AgentDyneError
54
+ attr_reader :fields
55
+
56
+ def initialize(message, fields: nil, raw: nil)
57
+ super(message, status_code: 400, code: "VALIDATION_ERROR", raw: raw)
58
+ @fields = fields || {}
59
+ end
60
+ end
61
+
62
+ # HTTP 429 — per-minute rate limit exceeded.
63
+ class RateLimitError < AgentDyneError
64
+ attr_reader :retry_after_seconds
65
+
66
+ def initialize(retry_after_seconds = 60.0, raw: nil)
67
+ super("Rate limit exceeded. Retry after #{retry_after_seconds.to_i}s",
68
+ status_code: 429, code: "RATE_LIMIT_EXCEEDED", raw: raw)
69
+ @retry_after_seconds = retry_after_seconds
70
+ end
71
+ end
72
+
73
+ # HTTP 429 / QUOTA_EXCEEDED — monthly execution quota exhausted.
74
+ class QuotaExceededError < AgentDyneError
75
+ attr_reader :plan
76
+
77
+ def initialize(plan = nil, raw: nil)
78
+ msg = plan ? "Monthly quota exceeded on \"#{plan}\" plan. Please upgrade." : "Monthly quota exceeded."
79
+ super(msg, status_code: 429, code: "QUOTA_EXCEEDED", raw: raw)
80
+ @plan = plan
81
+ end
82
+ end
83
+
84
+ # 5xx — unrecoverable server error.
85
+ class InternalServerError < AgentDyneError
86
+ def initialize(message = "Internal server error", raw: nil)
87
+ super(message, status_code: 500, code: "INTERNAL_SERVER_ERROR", raw: raw)
88
+ end
89
+ end
90
+
91
+ # Network-level failure (connection refused, DNS, TLS).
92
+ class NetworkError < AgentDyneError
93
+ def initialize(message, cause: nil)
94
+ super(message, code: "NETWORK_ERROR")
95
+ set_backtrace(cause&.backtrace)
96
+ end
97
+ end
98
+
99
+ # Webhook HMAC-SHA256 signature verification failed.
100
+ class WebhookSignatureError < AgentDyneError
101
+ def initialize(message = "Webhook signature verification failed")
102
+ super(message, code: "WEBHOOK_SIGNATURE_INVALID")
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+
7
+ module AgentDyne
8
+ # Internal HTTP client. Not intended for direct use.
9
+ class HttpClient # :nodoc:
10
+ SDK_VERSION = AgentDyne::VERSION
11
+ DEFAULT_TIMEOUT = 60
12
+ NON_RETRYABLE = [400, 401, 403, 404, 422].freeze
13
+
14
+ def initialize(api_key:, base_url:, timeout:, max_retries:)
15
+ @api_key = api_key
16
+ @base_url = URI(base_url.chomp("/"))
17
+ @timeout = timeout
18
+ @max_retries = max_retries
19
+ end
20
+
21
+ def get(path, params = {})
22
+ request_with_retry(:get, path, params: params)
23
+ end
24
+
25
+ def post(path, body = nil)
26
+ request_with_retry(:post, path, body: body)
27
+ end
28
+
29
+ def patch(path, body = nil)
30
+ request_with_retry(:patch, path, body: body)
31
+ end
32
+
33
+ def delete(path)
34
+ request_with_retry(:delete, path)
35
+ end
36
+
37
+ # Yields raw SSE data lines.
38
+ def stream(path, body, &block)
39
+ uri = build_uri(path)
40
+ http = build_http(uri)
41
+
42
+ req = Net::HTTP::Post.new(uri)
43
+ apply_headers(req, stream: true)
44
+ req.body = JSON.generate(body)
45
+
46
+ http.request(req) do |resp|
47
+ raise_for_status(resp) unless resp.is_a?(Net::HTTPSuccess)
48
+ resp.read_body do |chunk|
49
+ chunk.each_line do |line|
50
+ line = line.strip
51
+ next unless line.start_with?("data: ")
52
+ data = line[6..]
53
+ return if data == "[DONE]"
54
+ block.call(data)
55
+ end
56
+ end
57
+ end
58
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
59
+ raise NetworkError.new(e.message, cause: e)
60
+ end
61
+
62
+ private
63
+
64
+ def request_with_retry(method, path, params: {}, body: nil)
65
+ last_error = nil
66
+
67
+ (@max_retries + 1).times do |attempt|
68
+ if attempt > 0
69
+ sleep(backoff_delay(attempt - 1))
70
+ end
71
+
72
+ begin
73
+ result = execute_request(method, path, params: params, body: body)
74
+ return result
75
+ rescue RateLimitError => e
76
+ sleep(e.retry_after_seconds) if attempt < @max_retries
77
+ last_error = e
78
+ rescue AgentDyneError => e
79
+ raise if NON_RETRYABLE.include?(e.status_code)
80
+ raise if e.status_code && e.status_code < 500
81
+ last_error = e
82
+ rescue NetworkError => e
83
+ last_error = e
84
+ end
85
+ end
86
+
87
+ raise last_error
88
+ end
89
+
90
+ def execute_request(method, path, params: {}, body: nil)
91
+ uri = build_uri(path, params)
92
+ http = build_http(uri)
93
+ req = build_request(method, uri, body)
94
+
95
+ resp = http.request(req)
96
+ raise_for_status(resp)
97
+
98
+ body_str = resp.body
99
+ return nil if body_str.nil? || body_str.empty?
100
+ JSON.parse(body_str)
101
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
102
+ raise NetworkError.new(e.message, cause: e)
103
+ end
104
+
105
+ def build_uri(path, params = {})
106
+ uri = URI.join(@base_url.to_s + "/", path.delete_prefix("/"))
107
+ unless params.empty?
108
+ uri.query = URI.encode_www_form(params.compact)
109
+ end
110
+ uri
111
+ end
112
+
113
+ def build_http(uri)
114
+ http = Net::HTTP.new(uri.host, uri.port)
115
+ http.use_ssl = uri.scheme == "https"
116
+ http.read_timeout = @timeout
117
+ http.open_timeout = @timeout
118
+ http
119
+ end
120
+
121
+ def build_request(method, uri, body)
122
+ klass = {
123
+ get: Net::HTTP::Get,
124
+ post: Net::HTTP::Post,
125
+ patch: Net::HTTP::Patch,
126
+ delete: Net::HTTP::Delete,
127
+ }[method]
128
+ req = klass.new(uri)
129
+ apply_headers(req)
130
+ req.body = JSON.generate(body) if body
131
+ req
132
+ end
133
+
134
+ def apply_headers(req, stream: false)
135
+ req["Authorization"] = "Bearer #{@api_key}"
136
+ req["Content-Type"] = "application/json"
137
+ req["Accept"] = stream ? "text/event-stream" : "application/json"
138
+ req["User-Agent"] = "agentdyne-ruby/#{SDK_VERSION}"
139
+ req["X-SDK-Language"]= "ruby"
140
+ end
141
+
142
+ def raise_for_status(resp)
143
+ return if resp.is_a?(Net::HTTPSuccess)
144
+
145
+ status = resp.code.to_i
146
+ raw_body = resp.body.to_s
147
+ body = begin; JSON.parse(raw_body); rescue JSON::ParserError; {}; end
148
+
149
+ message = body["error"] || body["message"] || "HTTP #{status}"
150
+ code = body["code"]
151
+
152
+ case status
153
+ when 400 then raise ValidationError.new(message, fields: body["fields"], raw: body)
154
+ when 401 then raise AuthenticationError.new(message, raw: body)
155
+ when 403
156
+ if code == "SUBSCRIPTION_REQUIRED"
157
+ raise SubscriptionRequiredError.new(nil, raw: body)
158
+ end
159
+ raise PermissionDeniedError.new(message, raw: body)
160
+ when 404 then raise NotFoundError.new(message, raw: body)
161
+ when 429
162
+ if code == "QUOTA_EXCEEDED"
163
+ raise QuotaExceededError.new(nil, raw: body)
164
+ end
165
+ retry_after = (resp["Retry-After"] || "60").to_f
166
+ raise RateLimitError.new(retry_after, raw: body)
167
+ else
168
+ if status >= 500
169
+ raise InternalServerError.new(message, raw: body)
170
+ end
171
+ raise AgentDyneError.new(message, status_code: status, code: code, raw: body)
172
+ end
173
+ end
174
+
175
+ def backoff_delay(attempt)
176
+ base = 0.5
177
+ cap = 30.0
178
+ ceiling = [cap, base * (2**attempt)].min
179
+ rand * ceiling
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentDyne
4
+ VERSION = "1.0.0"
5
+ end
data/lib/agentdyne.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/agentdyne.rb — AgentDyne Ruby SDK main entry point.
4
+ #
5
+ # Quick start:
6
+ #
7
+ # require "agentdyne"
8
+ #
9
+ # client = AgentDyne::Client.new(api_key: "agd_...")
10
+ # result = client.execute("agent_id", "Summarize this email...")
11
+ # puts result.output
12
+ #
13
+
14
+ require_relative "agentdyne/version"
15
+ require_relative "agentdyne/errors"
16
+ require_relative "agentdyne/types"
17
+ require_relative "agentdyne/http"
18
+ require_relative "agentdyne/client"
19
+
20
+ module AgentDyne
21
+ # Convenience constructor — mirrors Python and Node patterns.
22
+ #
23
+ # client = AgentDyne.new(api_key: "agd_...")
24
+ #
25
+ def self.new(**kwargs)
26
+ Client.new(**kwargs)
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agentdyne
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - AgentDyne, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.23'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.23'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.2'
69
+ description: Discover, execute, and monetise AI agents with the AgentDyne Ruby SDK.
70
+ Zero required dependencies.
71
+ email:
72
+ - sdk@agentdyne.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - README.md
78
+ - agentdyne.gemspec
79
+ - lib/agentdyne.rb
80
+ - lib/agentdyne/client.rb
81
+ - lib/agentdyne/errors.rb
82
+ - lib/agentdyne/http.rb
83
+ - lib/agentdyne/version.rb
84
+ homepage: https://agentdyne.com
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://agentdyne.com
89
+ source_code_uri: https://github.com/agentdyne/sdk-ruby
90
+ bug_tracker_uri: https://github.com/agentdyne/sdk-ruby/issues
91
+ documentation_uri: https://agentdyne.com/docs
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '3.1'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.7.6.3
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Official Ruby SDK for AgentDyne — The Global Microagent Marketplace
112
+ test_files: []