agent-harness 0.39.0 → 0.40.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b6b9b84a3074d82a44dd35a6b377628c64cfa01958ca5c41ec910b694375fcf
4
- data.tar.gz: 8069427d9a79624deba026984490a5f25cf97df7364c6d78b44c2a43aef773c6
3
+ metadata.gz: ee00d0e61838facc87483e6bdcac764ed26dc5bb227a15075b4e8f0daa8983d4
4
+ data.tar.gz: 6034f1733639711ad2b7df6bf9c26da839d45c26d1ddb6b0b4e5ba413854fc8a
5
5
  SHA512:
6
- metadata.gz: cad34514f544dcf13a470afbb7cbdde82269246ee16d97319e3e6f33afd2e663b0625fd2f088bc4c68ef491676ac3c5cfc80e6bc3ba5a030fbda1abbe9a04cf7
7
- data.tar.gz: 48fd85599fca1ae94fe13e433d4a69d1ab5bd8ce4c9c8feab6388c79c2d48c5314467cba8c4fcb277662c1aea20f53206b35fef6cad0a82ef82618249a80a883
6
+ metadata.gz: 130a2897522e8b1aa73b2a9cf6094bd24b3b529696445e84356a78faa02540e0c4c2677602e306fca60e1f1a3040e48c04b3ff0a9ca28f144a24de6fc3fa5887
7
+ data.tar.gz: 647ab39ee27728d28a3441ccbbd84edd3521e7e434a120ac236e411849b22aa431ae7fc6dbe499ec8c525aee6159c9fdba42a5ee8ff7ec329281dd7ea3a4ad33
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.39.0"
2
+ ".": "0.40.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
  * add runner model compatibility contract (`AgentHarness.model_compatibility`) with structured `ModelCompatibility::Result` outcomes. Codex exposes static facts for CLI-gated models (e.g. `gpt-5.5` requires Codex CLI `>= 0.116.0`), a baseline supported-model list, supported auth modes, and a `DEFAULT_COMPATIBLE_MODEL_ID` fallback so downstream orchestrators can validate tier/model assignments before scheduling agent runs ([#259](https://github.com/viamin/agent-harness/issues/259)).
6
6
  * **auth:** add provider-owned PKCE code-exchange API for Claude OAuth (`AgentHarness::Authentication.exchange_code`). Takes an authorization code plus PKCE verifier (and `redirect_uri`/`client_id`), posts an `authorization_code` grant to the Claude token endpoint, and persists the resulting access/refresh tokens in the native `claudeAiOauth` shape. Adds `exchange_code_supported?` and a `code_exchange` key to `auth_capabilities` ([#266](https://github.com/viamin/agent-harness/issues/266)).
7
7
 
8
+ ## [0.40.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.39.0...agent-harness/v0.40.0) (2026-09-25)
9
+
10
+
11
+ ### Features
12
+
13
+ * Provide Native Embedding Support (RDR-072) ([#439](https://github.com/viamin/agent-harness/issues/439)) ([551378d](https://github.com/viamin/agent-harness/commit/551378d11627a5227059032f0b7418dc166f54e9))
14
+
8
15
  ## [0.39.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.38.0...agent-harness/v0.39.0) (2026-09-25)
9
16
 
10
17
 
data/README.md CHANGED
@@ -39,6 +39,71 @@ puts response.output
39
39
  response = AgentHarness.send_message("Explain this code", provider: :cursor)
40
40
  ```
41
41
 
42
+ ## Native Embeddings
43
+
44
+ `AgentHarness.embed` sends a whole input batch through RubyLLM and returns one
45
+ vector per input, in the same order. Credentials, endpoint, headers, timeout,
46
+ and retry limits are request-local; they do not change `RubyLLM.config` or the
47
+ CLI/subscription provider configuration.
48
+
49
+ ```ruby
50
+ result = AgentHarness.embed(
51
+ inputs: ["first document", "second document"],
52
+ model: "text-embedding-3-small",
53
+ dimensions: 512,
54
+ endpoint: "https://api.openai.com/v1", # optional OpenAI-compatible base URL
55
+ credentials: {api_key: ENV.fetch("EMBEDDING_API_KEY")},
56
+ headers: {"X-Tenant-ID" => tenant.external_id},
57
+ timeout: 30,
58
+ max_attempts: 3,
59
+ cancellation: -> { request_cancelled? }
60
+ )
61
+
62
+ result.vectors # one vector for each input
63
+ result.usage # { input_tokens: 42 }
64
+ ```
65
+
66
+ `credentials` may also be the API key string. Extra headers cannot replace the
67
+ `Authorization` header; change credentials explicitly instead. `max_attempts`
68
+ includes the initial request. Agent Harness performs the only retry loop: 429,
69
+ timeout/connection, and transient 5xx failures are retried up to that bound,
70
+ while 401 and 403 responses fail immediately. The harness honors `Retry-After`
71
+ within its bounded retry policy. A cancellation callable is checked immediately
72
+ before every physical HTTP attempt.
73
+
74
+ Usage is the provider-reported total for the complete batch. When the provider
75
+ omits usage, `result.usage[:input_tokens]` remains `nil`. The harness does not
76
+ estimate usage or allocate a batch total across vectors, so
77
+ `result.per_vector_usage` is always `nil`.
78
+
79
+ Authentication failures raise `AgentHarness::AuthenticationError`, exhausted
80
+ rate limits raise `AgentHarness::RateLimitError`, timeouts raise
81
+ `AgentHarness::TimeoutError`, transient provider failures raise
82
+ `AgentHarness::ProviderError`, cancellations raise
83
+ `AgentHarness::CancelledError`, and incomplete or invalid vector batches raise
84
+ `AgentHarness::MalformedEmbeddingError`. Empty input returns an empty result
85
+ without contacting the provider.
86
+
87
+ ### Migrating from Paid transport patches
88
+
89
+ This operation replaces downstream host/container embedding transport
90
+ extensions for OpenAI-compatible direct and proxy endpoints. After adopting an
91
+ agent-harness release containing this capability:
92
+
93
+ 1. Run the downstream embedding contract suite against both the direct provider
94
+ and proxy endpoint, including tenant-specific credentials and headers.
95
+ 2. Verify the released gem artifact includes `AgentHarness.embed` and record the
96
+ passing artifact version or digest. Issue closure or a Git tag alone is not
97
+ release evidence.
98
+ 3. Switch only the embedding call site to `AgentHarness.embed`; leave unrelated
99
+ chat, schema, CLI, and subscription paths unchanged.
100
+ 4. Remove the downstream embedding request/parser/retry patch so Agent Harness
101
+ owns the single bounded retry loop. Keep durable workflow recovery and
102
+ accounting in the downstream application.
103
+
104
+ The runtime dependency is Ruby 3.2 or newer and RubyLLM 2.x. No Rails database
105
+ or RubyLLM persistence tables are required for this plain-Ruby operation.
106
+
42
107
  ## Configuration
43
108
 
44
109
  ### Ruby DSL
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/net_http"
5
+ require "json"
6
+
7
+ module AgentHarness
8
+ # Builds a request-local Faraday adapter for headers and cancellation.
9
+ module EmbeddingAdapter
10
+ module_function
11
+
12
+ def build(headers:, cancellation: nil)
13
+ Class.new(Faraday::Adapter::NetHttp) do
14
+ define_method(:call) do |env|
15
+ raise CancelledError, "Embedding request cancelled" if cancellation&.call
16
+
17
+ env.request_headers.update(headers)
18
+ super(env).on_complete { |response| EmbeddingAdapter.order_rows(response) }
19
+ end
20
+ end
21
+ end
22
+
23
+ def order_rows(response)
24
+ payload = JSON.parse(response.body)
25
+ rows = payload["data"]
26
+ return unless rows.is_a?(Array)
27
+
28
+ indices = rows.map { |row| row["index"] if row.is_a?(Hash) }
29
+ unless indices.all?(Integer) && indices.sort == (0...rows.length).to_a
30
+ raise MalformedEmbeddingError, "Provider returned invalid embedding indices"
31
+ end
32
+
33
+ payload["data"] = rows.sort_by { |row| row.fetch("index") }
34
+ response.body = JSON.generate(payload)
35
+ rescue JSON::ParserError
36
+ nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentHarness
4
+ # Normalized result returned by AgentHarness.embed.
5
+ class EmbeddingResult
6
+ attr_reader :vectors, :model, :usage, :attempts
7
+
8
+ def initialize(vectors:, model:, input_tokens: nil, attempts: [])
9
+ @vectors = vectors
10
+ @model = model
11
+ @usage = {input_tokens: input_tokens}.freeze
12
+ @attempts = attempts.freeze
13
+ end
14
+
15
+ # Batch usage is never guessed or divided among individual vectors.
16
+ def per_vector_usage
17
+ nil
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+ require "securerandom"
5
+ require "time"
6
+
7
+ module AgentHarness
8
+ # Provider-neutral embedding execution backed by RubyLLM.
9
+ class Embeddings
10
+ DEFAULT_TIMEOUT = 300
11
+ DEFAULT_MAX_ATTEMPTS = 3
12
+ RETRY_BASE_DELAY = 0.25
13
+ RETRY_MAX_DELAY = 2.0
14
+ CANCELLATION_POLL_INTERVAL = 0.05
15
+ TRANSIENT_ERRORS = [RateLimitError, TimeoutError].freeze
16
+
17
+ def initialize(model:, credentials:, endpoint: nil, headers: {}, timeout: DEFAULT_TIMEOUT,
18
+ max_attempts: DEFAULT_MAX_ATTEMPTS, cancellation: nil, observer: nil, request_id: nil)
19
+ @model = model
20
+ @api_key = credential(credentials, :api_key)
21
+ @endpoint = endpoint
22
+ @headers = headers.to_h.transform_keys(&:to_s).freeze
23
+ @timeout = timeout
24
+ @max_attempts = max_attempts
25
+ @cancellation = cancellation
26
+ @observer = observer
27
+ @request_id = request_id || SecureRandom.uuid
28
+ validate!
29
+ end
30
+
31
+ def call(inputs:, dimensions: nil)
32
+ inputs = Array(inputs)
33
+ return EmbeddingResult.new(vectors: [], model: @model) if inputs.empty?
34
+
35
+ execute(inputs, dimensions)
36
+ end
37
+
38
+ private
39
+
40
+ def execute(inputs, dimensions)
41
+ attempts = []
42
+
43
+ 1.upto(@max_attempts) do |number|
44
+ check_cancellation!
45
+ result = attempt(inputs, dimensions, number, attempts)
46
+ return result
47
+ rescue RateLimitError, TimeoutError, ProviderError => e
48
+ raise unless retryable?(e) && number < @max_attempts
49
+
50
+ wait_before_retry(e, number)
51
+ end
52
+ end
53
+
54
+ def attempt(inputs, dimensions, number, attempts)
55
+ started_at = Time.now.utc
56
+ embedding = request_embedding(inputs, dimensions)
57
+ validate_result!(embedding.vectors, inputs.length)
58
+ report = success_report(embedding, number, started_at)
59
+ rescue CancelledError => e
60
+ record_attempt(attempts, failure_report(e, number, started_at, :cancelled, :cancelled))
61
+ raise
62
+ rescue AuthenticationError, AuthorizationError, RateLimitError, TimeoutError, ProviderError => e
63
+ record_attempt(attempts, failure_report(e, number, started_at, *error_classification(e)))
64
+ raise
65
+ rescue NoMethodError, TypeError => e
66
+ error = MalformedEmbeddingError.new("Malformed embedding response", original_error: e)
67
+ record_attempt(attempts, failure_report(error, number, started_at, *error_classification(error)))
68
+ raise error
69
+ else
70
+ record_attempt(attempts, report)
71
+ EmbeddingResult.new(
72
+ vectors: embedding.vectors, model: embedding.model,
73
+ input_tokens: embedding.tokens.input, attempts: attempts
74
+ )
75
+ end
76
+
77
+ def request_embedding(inputs, dimensions)
78
+ context.embed(
79
+ inputs,
80
+ model: @model,
81
+ provider: :openai,
82
+ assume_model_exists: true,
83
+ dimensions: dimensions
84
+ )
85
+ rescue RubyLLM::UnauthorizedError => e
86
+ raise AuthenticationError.new(e.message, provider: :openai, original_error: e)
87
+ rescue RubyLLM::ForbiddenError => e
88
+ raise AuthorizationError.new(e.message, provider: :openai, original_error: e)
89
+ rescue RubyLLM::RateLimitError => e
90
+ raise RateLimitError.new(e.message, provider: :openai, reset_time: retry_after(e), original_error: e)
91
+ rescue Faraday::TimeoutError, Timeout::Error => e
92
+ raise TimeoutError.new(e.message, original_error: e)
93
+ rescue Faraday::ConnectionFailed => e
94
+ raise connection_error(e)
95
+ rescue RubyLLM::ServerError, RubyLLM::ServiceUnavailableError, RubyLLM::OverloadedError => e
96
+ raise ProviderError.new(e.message, original_error: e)
97
+ rescue Faraday::ParsingError, NoMethodError, TypeError => e
98
+ raise MalformedEmbeddingError.new("Malformed embedding response", original_error: e)
99
+ rescue RubyLLM::Error => e
100
+ raise ProviderError.new(e.message, original_error: e)
101
+ end
102
+
103
+ def context
104
+ RubyLLM.context do |config|
105
+ config.openai_api_key = @api_key
106
+ config.openai_api_base = @endpoint if @endpoint
107
+ config.request_timeout = @timeout
108
+ config.max_retries = 0
109
+ config.retry_interval_randomness = 0
110
+ config.faraday_adapter = EmbeddingAdapter.build(headers: @headers, cancellation: @cancellation)
111
+ end
112
+ end
113
+
114
+ def success_report(embedding, number, started_at)
115
+ attempt_report(number, started_at).merge(
116
+ status: :succeeded,
117
+ model: embedding.model,
118
+ usage: usage(embedding.tokens.input),
119
+ provider_reported: !embedding.tokens.input.nil?,
120
+ error: nil
121
+ ).freeze
122
+ end
123
+
124
+ def failure_report(error, number, started_at, category, code)
125
+ attempt_report(number, started_at).merge(
126
+ status: (category == :cancelled) ? :cancelled : :failed,
127
+ usage: usage(nil),
128
+ provider_reported: false,
129
+ error: {category: category, code: code}.freeze
130
+ ).freeze
131
+ end
132
+
133
+ def attempt_report(number, started_at)
134
+ {
135
+ attempt_id: "attempt_#{SecureRandom.uuid}", request_id: @request_id,
136
+ number: number, provider: :openai, model: @model,
137
+ started_at: started_at.iso8601(6), finished_at: Time.now.utc.iso8601(6),
138
+ cost: nil, provider_request_id: nil
139
+ }
140
+ end
141
+
142
+ def usage(input_tokens)
143
+ {input_tokens: input_tokens, output_tokens: nil, total_tokens: input_tokens}.freeze
144
+ end
145
+
146
+ def record_attempt(attempts, report)
147
+ attempts << report
148
+ return unless @observer
149
+
150
+ @observer.respond_to?(:on_attempt) ? @observer.on_attempt(report) : @observer.call(report)
151
+ end
152
+
153
+ def error_classification(error)
154
+ return [error.error_category, error.error_code] if error.is_a?(AuthenticationError) || error.is_a?(AuthorizationError)
155
+ return [:transient, :rate_limited] if error.is_a?(RateLimitError)
156
+ return [:transient, :timeout] if error.is_a?(TimeoutError)
157
+ return transient_provider_classification(error) if transient_provider_error?(error)
158
+ return [:invalid_response, :malformed_response] if error.is_a?(MalformedEmbeddingError)
159
+
160
+ [:unknown, :unclassified_provider_error]
161
+ end
162
+
163
+ def transient_provider_classification(error)
164
+ original = error.original_error
165
+ return [:transient, :connection_failed] if original.is_a?(Faraday::ConnectionFailed)
166
+ return [:transient, :service_unavailable] if original.is_a?(RubyLLM::ServiceUnavailableError)
167
+ return [:transient, :overloaded] if original.is_a?(RubyLLM::OverloadedError)
168
+
169
+ [:transient, :server_error]
170
+ end
171
+
172
+ def retryable?(error)
173
+ TRANSIENT_ERRORS.any? { |klass| error.is_a?(klass) } || transient_provider_error?(error)
174
+ end
175
+
176
+ def transient_provider_error?(error)
177
+ original = error.original_error
178
+ original.is_a?(Faraday::ConnectionFailed) || original.is_a?(RubyLLM::ServerError) ||
179
+ original.is_a?(RubyLLM::ServiceUnavailableError) || original.is_a?(RubyLLM::OverloadedError)
180
+ end
181
+
182
+ def wait_before_retry(error, attempt_number)
183
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + retry_delay(error, attempt_number)
184
+ loop do
185
+ check_cancellation!
186
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
187
+ break unless remaining.positive?
188
+
189
+ sleep([remaining, CANCELLATION_POLL_INTERVAL].min)
190
+ end
191
+ end
192
+
193
+ def retry_delay(error, attempt_number)
194
+ retry_after_delay = error.reset_time - Time.now if error.is_a?(RateLimitError) && error.reset_time
195
+ return retry_after_delay if retry_after_delay&.positive?
196
+
197
+ [RETRY_BASE_DELAY * (2**(attempt_number - 1)), RETRY_MAX_DELAY].min
198
+ end
199
+
200
+ def check_cancellation!
201
+ raise CancelledError, "Embedding request cancelled" if @cancellation&.call
202
+ end
203
+
204
+ def validate_result!(vectors, expected_count)
205
+ valid = vectors.is_a?(Array) && vectors.length == expected_count
206
+ valid &&= vectors.all? { |vector| valid_vector?(vector) }
207
+ raise MalformedEmbeddingError, "Provider returned an invalid embedding batch" unless valid
208
+ end
209
+
210
+ def valid_vector?(vector)
211
+ vector.is_a?(Array) && !vector.empty? && vector.all? { |value| value.is_a?(Numeric) && value.finite? }
212
+ end
213
+
214
+ def credential(credentials, key)
215
+ return credentials if credentials.is_a?(String)
216
+
217
+ credentials&.[](key) || credentials&.[](key.to_s)
218
+ end
219
+
220
+ def validate!
221
+ raise ArgumentError, "model must be a non-empty string" unless @model.is_a?(String) && !@model.empty?
222
+ raise ArgumentError, "credentials must include api_key" unless @api_key.is_a?(String) && !@api_key.empty?
223
+ if @headers.keys.any? { |header| header.casecmp?("authorization") }
224
+ raise ArgumentError, "headers cannot override Authorization; use credentials"
225
+ end
226
+ raise ArgumentError, "timeout must be positive" unless @timeout.is_a?(Numeric) && @timeout.positive?
227
+ unless @max_attempts.is_a?(Integer) && @max_attempts.positive?
228
+ raise ArgumentError, "max_attempts must be a positive integer"
229
+ end
230
+ unless @request_id.is_a?(String) && !@request_id.empty?
231
+ raise ArgumentError, "request_id must be a non-empty string"
232
+ end
233
+ unless @observer.nil? || @observer.respond_to?(:on_attempt) || @observer.respond_to?(:call)
234
+ raise ArgumentError, "observer must respond to on_attempt or call"
235
+ end
236
+ end
237
+
238
+ def retry_after(error)
239
+ value = error.response&.response_headers&.[]("retry-after")
240
+ return unless value
241
+
242
+ seconds = Float(value, exception: false)
243
+ seconds ? Time.now + seconds : Time.httpdate(value)
244
+ rescue ArgumentError, TypeError
245
+ nil
246
+ end
247
+
248
+ def connection_error(error)
249
+ wrapped = error.wrapped_exception
250
+ if wrapped.is_a?(Timeout::Error)
251
+ TimeoutError.new(error.message, original_error: error)
252
+ else
253
+ ProviderError.new(error.message, original_error: error)
254
+ end
255
+ end
256
+ end
257
+ end
@@ -15,6 +15,9 @@ module AgentHarness
15
15
  # Provider-related errors
16
16
  class ProviderError < Error; end
17
17
 
18
+ # Raised when an embedding provider response cannot satisfy the batch contract.
19
+ class MalformedEmbeddingError < ProviderError; end
20
+
18
21
  class ProviderInstallationError < ProviderError
19
22
  attr_reader :provider, :error_category
20
23
 
@@ -39,6 +42,9 @@ module AgentHarness
39
42
 
40
43
  class CommandExecutionError < Error; end
41
44
 
45
+ # Raised when a caller cancels a request before a transport attempt.
46
+ class CancelledError < Error; end
47
+
42
48
  # Rate limiting and circuit breaker errors
43
49
  class RateLimitError < Error
44
50
  attr_reader :reset_time, :provider, :error_category
@@ -62,10 +68,24 @@ module AgentHarness
62
68
 
63
69
  # Authentication errors
64
70
  class AuthenticationError < Error
65
- attr_reader :provider
71
+ attr_reader :provider, :error_category, :error_code
66
72
 
67
- def initialize(message = nil, provider: nil, **kwargs)
73
+ def initialize(message = nil, provider: nil, error_category: :authentication, error_code: :invalid_credential, **kwargs)
74
+ @provider = provider
75
+ @error_category = error_category
76
+ @error_code = error_code
77
+ super(message, **kwargs)
78
+ end
79
+ end
80
+
81
+ # Raised when valid credentials do not grant access to the requested resource.
82
+ class AuthorizationError < Error
83
+ attr_reader :provider, :error_category, :error_code
84
+
85
+ def initialize(message = nil, provider: nil, error_category: :authorization, error_code: :permission_denied, **kwargs)
68
86
  @provider = provider
87
+ @error_category = error_category
88
+ @error_code = error_code
69
89
  super(message, **kwargs)
70
90
  end
71
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.39.0"
4
+ VERSION = "0.40.0"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -87,6 +87,12 @@ module AgentHarness
87
87
  conductor.send_message(prompt, provider: provider, executor: executor, **options)
88
88
  end
89
89
 
90
+ # Generate embeddings for a batch of strings through a request-local RubyLLM context.
91
+ # @return [EmbeddingResult] vectors in input order and provider-reported batch usage
92
+ def embed(inputs:, model:, credentials:, dimensions: nil, **options)
93
+ Embeddings.new(model: model, credentials: credentials, **options).call(inputs: inputs, dimensions: dimensions)
94
+ end
95
+
90
96
  # Resolve a canonical extension definition by name or inline object.
91
97
  #
92
98
  # @param reference [Symbol, String, Extensions::Base]
@@ -456,6 +462,9 @@ require_relative "agent_harness/configuration"
456
462
  require_relative "agent_harness/command_executor"
457
463
  require_relative "agent_harness/docker_command_executor"
458
464
  require_relative "agent_harness/response"
465
+ require_relative "agent_harness/embedding_result"
466
+ require_relative "agent_harness/embedding_adapter"
467
+ require_relative "agent_harness/embeddings"
459
468
  require_relative "agent_harness/token_tracker"
460
469
  require_relative "agent_harness/token_usage_tracker"
461
470
  require_relative "agent_harness/error_taxonomy"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.0
4
+ version: 0.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -29,6 +29,26 @@ dependencies:
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: ruby_llm
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
32
52
  - !ruby/object:Gem::Dependency
33
53
  name: rake
34
54
  requirement: !ruby/object:Gem::Requirement
@@ -71,6 +91,20 @@ dependencies:
71
91
  - - "~>"
72
92
  - !ruby/object:Gem::Version
73
93
  version: '1.3'
94
+ - !ruby/object:Gem::Dependency
95
+ name: webmock
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '3.0'
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '3.0'
74
108
  description: |
75
109
  AgentHarness provides a unified interface for CLI-based AI coding agents like
76
110
  Claude Code, Cursor, Gemini CLI, and others. It offers full orchestration with
@@ -107,6 +141,9 @@ files:
107
141
  - lib/agent_harness/conversation.rb
108
142
  - lib/agent_harness/dependency_updater.rb
109
143
  - lib/agent_harness/docker_command_executor.rb
144
+ - lib/agent_harness/embedding_adapter.rb
145
+ - lib/agent_harness/embedding_result.rb
146
+ - lib/agent_harness/embeddings.rb
110
147
  - lib/agent_harness/error_taxonomy.rb
111
148
  - lib/agent_harness/errors.rb
112
149
  - lib/agent_harness/execution_preparation.rb