agent-harness 0.41.0 → 0.42.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: c4406422d8b90ec6406bb4cdcc6e1d7496b708f43aad306d258c2570ed4f710e
4
- data.tar.gz: cd23fe2f7d4b05e2ac18ac9aa2a631a0b292bc2703ae092f4f55a1ca6f3febbb
3
+ metadata.gz: 9c2a3cf60957839163bbda4f4e202f663ee68915e49ba3853abce85b5f6f16e9
4
+ data.tar.gz: b5a9950414fc72fe2626abbb3ef50275e2e28267346414d04d330d3ea37c1297
5
5
  SHA512:
6
- metadata.gz: efb5e61347d70b28e03bb0cd2843429928843147bf8f196d1292af97be97c8ec67ca4ef81e6f169562d1fac5e88e277b06d07a7cd44bf3d85a772effb2c46051
7
- data.tar.gz: 7c5fb37c7e14edbc3da68f74dc3f1a6d10e04630e70509a79bd04b9ebbfde2397d6a4753e288dd33d962efae79003728d30cdb0ae113d1bd4864de5995daa112
6
+ metadata.gz: b6147210553af7975d99dbf53cc52c0b37e7ba25c8424a9cfb24a290bae9172d25bcbc8a6e5b489d1b2ae363251facc756ea0eaae55a6efd3904cdb64fcda025
7
+ data.tar.gz: f869749041ff450642b8ba940a3f0f9f8ea2cd7a3d788b2414ae7f0115dcb5fde4dcb7f5aa725b4e1ba2216568a1412e2bf5439c47b61bf75b2d06390faebfae
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.41.0"
2
+ ".": "0.42.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.42.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.41.0...agent-harness/v0.42.0) (2026-09-25)
9
+
10
+
11
+ ### Features
12
+
13
+ * Expose Attempt-Level Usage and Cost (RDR-072) ([#443](https://github.com/viamin/agent-harness/issues/443)) ([818f8ca](https://github.com/viamin/agent-harness/commit/818f8ca0f873f002fc4d26b2a864238ae41fa866))
14
+
8
15
  ## [0.41.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.40.0...agent-harness/v0.41.0) (2026-09-25)
9
16
 
10
17
 
@@ -8,8 +8,8 @@ usage, and optional conversation-persistence work.
8
8
  ## Status and rollout boundary
9
9
 
10
10
  RDR-072's rollout guard was **docs-only** for the design phase. The normalized
11
- chat capability described in "Shipped normalized chat surface" below is now
12
- implemented. Other capabilities remain design contracts and each still needs
11
+ chat and attempt-accounting capabilities described below are now implemented
12
+ behind `AgentHarness::Api::ChatTransport`. Other capabilities remain design contracts and each still needs
13
13
  its own failing-first contract tests, implementation, release evidence, and
14
14
  downstream adoption evidence before a caller enables it.
15
15
 
@@ -336,6 +336,23 @@ observer so durable accounting can persist an attempt even when no message is
336
336
  created. Callers deduplicate on `attempt_id`. Cost identifies its source as
337
337
  provider-reported or harness-estimated; unknown cost remains `nil`.
338
338
 
339
+ The observer receives `attempt_completed` after every physical request, with
340
+ the same report later returned in `result[:attempts]`. Delivery is at least
341
+ once across process recovery: consumers MUST enforce a unique key on
342
+ `attempt_id` and treat a repeated report as an idempotent upsert, not another
343
+ charge. `AttemptReport.from_h` restores JSON-decoded reports. Stored cost
344
+ includes USD component amounts, `source` (`provider_reported` or `estimated`),
345
+ and `priced_at`; restoration never consults current prices. Missing counts and
346
+ prices remain absent/`nil`, while a reported zero remains zero. Usage may also
347
+ include `cache_read_tokens`, `cache_write_tokens`, and `thinking_tokens`.
348
+
349
+ Attempt events contain only identifiers, provider/model, outcome, normalized
350
+ usage/cost, timestamps, and sanitized classified errors. They do not include
351
+ prompts, messages, request headers, endpoints, or credentials. RubyLLM API chat
352
+ requests are in this ledger. Existing CLI providers, `TextTransport`,
353
+ `OpenAICompatibleTransport`, token trackers, embeddings, and other
354
+ non-`Api::ChatTransport` paths remain outside it.
355
+
339
356
  Only errors classified `transient` are eligible for bounded request retry:
340
357
  connection failure, timeout before a partial stream, rate limit, server error,
341
358
  service unavailable, and overload. Authentication, authorization, billing,
@@ -569,6 +586,27 @@ types stay behind the harness boundary.
569
586
 
570
587
  ## Compatibility and release evidence
571
588
 
589
+ ### Attempt-accounting capability evidence
590
+
591
+ - Release: pending the first published version containing issue #435; a Git
592
+ branch or tag alone is not downstream adoption evidence.
593
+ - Scope: normalized chat through `Api::ChatTransport` for Anthropic Messages
594
+ and OpenAI Responses/Chat Completions (including compatible endpoints), with
595
+ request-local API-key authentication. It is stacked on the normalized chat
596
+ capability from #433.
597
+ - Verification: the API contract specs cover request-local credentials,
598
+ endpoint/header isolation, error classification, bounded non-nested retry,
599
+ fallback, cancellation, partial usage, cache usage, repeated identity,
600
+ observer redaction, and JSON reload with preserved pricing. The full upstream
601
+ suite and lint run on the repository's supported Ruby environment.
602
+ - Migration: no Rails tables or migrations are loaded or required. Paid and
603
+ agent-image consumer versions remain unverified and MUST NOT adopt this
604
+ capability until their integration suites record the exact released gem and
605
+ image versions.
606
+ - Retained paths: CLI/subscription providers, legacy HTTP transports,
607
+ embeddings, and token trackers remain outside this ledger and require
608
+ separate migration issues.
609
+
572
610
  AgentHarness currently supports Ruby 3.2 and later and must remain usable as a
573
611
  plain Ruby gem. RubyLLM 2.0.0 itself supports Ruby 3.1.3 and later, but adding it
574
612
  would introduce Faraday, event-stream parsing, Schematist, Marcel, and Zeitwerk
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module AgentHarness
6
+ module Api
7
+ # Serializable accounting facts for one physical provider request.
8
+ class AttemptReport
9
+ STATUSES = %i[succeeded failed cancelled partial].freeze
10
+ USAGE_KEYS = %i[input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens total_tokens].freeze
11
+ COST_KEYS = %i[input output cache_read cache_write thinking total].freeze
12
+
13
+ attr_reader :attributes
14
+
15
+ def self.from_h(attributes)
16
+ new(**symbolize(attributes))
17
+ end
18
+
19
+ def self.symbolize(value)
20
+ return value.to_h { |key, child| [key.to_sym, symbolize(child)] } if value.is_a?(Hash)
21
+ return value.map { |child| symbolize(child) } if value.is_a?(Array)
22
+
23
+ value
24
+ end
25
+ private_class_method :symbolize
26
+
27
+ def initialize(attempt_id:, request_id:, number:, provider:, model:, status:, started_at:, finished_at:,
28
+ usage: nil, cost: nil, provider_reported: false, error: nil)
29
+ @attributes = {
30
+ attempt_id: attempt_id.to_s, request_id: request_id.to_s, number: number,
31
+ provider: provider.to_sym, model: model&.to_s, status: status.to_sym,
32
+ started_at: timestamp(started_at), finished_at: timestamp(finished_at),
33
+ usage: normalize_usage(usage), cost: normalize_cost(cost),
34
+ provider_reported: provider_reported == true, error: error
35
+ }
36
+ validate!
37
+ deep_freeze(@attributes)
38
+ end
39
+
40
+ def to_h
41
+ deep_dup(attributes)
42
+ end
43
+
44
+ private
45
+
46
+ def timestamp(value)
47
+ value.respond_to?(:iso8601) ? value.iso8601(6) : Time.iso8601(value.to_s).utc.iso8601(6)
48
+ end
49
+
50
+ def normalize_usage(usage)
51
+ return unless usage
52
+
53
+ normalized = self.class.send(:symbolize, usage)
54
+ USAGE_KEYS.each_with_object({}) do |key, result|
55
+ result[key] = normalized[key] if normalized.key?(key)
56
+ end
57
+ end
58
+
59
+ def normalize_cost(cost)
60
+ return unless cost
61
+
62
+ normalized = self.class.send(:symbolize, cost)
63
+ amounts = COST_KEYS.to_h { |key| [key, normalized[key]] }
64
+ amounts.merge(currency: normalized.fetch(:currency, "USD"), source: normalized.fetch(:source).to_sym,
65
+ priced_at: timestamp(normalized.fetch(:priced_at)))
66
+ end
67
+
68
+ def validate!
69
+ raise ArgumentError, "attempt_id is required" if attributes[:attempt_id].empty?
70
+ raise ArgumentError, "request_id is required" if attributes[:request_id].empty?
71
+ raise ArgumentError, "number must be a positive integer" unless attributes[:number].is_a?(Integer) && attributes[:number].positive?
72
+ raise ArgumentError, "unknown attempt status" unless STATUSES.include?(attributes[:status])
73
+ end
74
+
75
+ def deep_freeze(value)
76
+ value.each_value { |child| deep_freeze(child) } if value.is_a?(Hash)
77
+ value.each { |child| deep_freeze(child) } if value.is_a?(Array)
78
+ value.freeze
79
+ end
80
+
81
+ def deep_dup(value)
82
+ return value.to_h { |key, child| [key, deep_dup(child)] } if value.is_a?(Hash)
83
+ return value.map { |child| deep_dup(child) } if value.is_a?(Array)
84
+
85
+ value
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "ruby_llm"
5
+ require_relative "attempt_report"
5
6
  require_relative "ruby_llm_chat_adapter"
6
7
 
7
8
  module AgentHarness
@@ -85,6 +86,18 @@ module AgentHarness
85
86
  end
86
87
 
87
88
  def perform_attempt(candidate)
89
+ accounting = nil
90
+ provider_usage = -> { !streamed_usage.nil? }
91
+ prepared_chat = @adapter.prepare(
92
+ candidate: candidate,
93
+ messages: request[:messages],
94
+ tools: request[:tools] || [],
95
+ max_output_tokens: request[:max_output_tokens],
96
+ temperature: request[:temperature],
97
+ timeout: request[:timeout],
98
+ on_accounting: ->(facts) { accounting = facts },
99
+ provider_usage: provider_usage
100
+ )
88
101
  attempt_id = @id_generator.call
89
102
  started_at = Time.now.utc
90
103
  emitted = false
@@ -99,7 +112,9 @@ module AgentHarness
99
112
  temperature: request[:temperature],
100
113
  stream: request[:stream] == true,
101
114
  timeout: request[:timeout],
102
- cancellation: request[:cancellation]
115
+ cancellation: request[:cancellation],
116
+ on_accounting: ->(facts) { accounting = facts },
117
+ prepared_chat: prepared_chat
103
118
  ) do |event|
104
119
  event = normalize_stream_event(event)
105
120
  emitted = true if output_event?(event)
@@ -109,15 +124,21 @@ module AgentHarness
109
124
  end
110
125
  raise RubyLLM::CancelledError unless active?
111
126
 
112
- success(candidate, attempt_id, started_at, adapter_result)
127
+ success(candidate, attempt_id, started_at, adapter_result, accounting)
113
128
  rescue ObserverError
114
129
  raise
115
130
  rescue => error
116
- failure(candidate, attempt_id, started_at, classify(error), partial: emitted, usage: streamed_usage)
131
+ return failed_result(classify(error), candidate) unless attempt_id
132
+
133
+ failure(candidate, attempt_id, started_at, classify(error), partial: emitted,
134
+ accounting: accounting || {
135
+ usage: streamed_usage, provider_reported: !streamed_usage.nil?
136
+ })
117
137
  end
118
138
 
119
- def success(candidate, attempt_id, started_at, adapter_result)
120
- attempts << attempt_report(candidate, attempt_id, started_at, :succeeded, usage: adapter_result[:usage])
139
+ def success(candidate, attempt_id, started_at, adapter_result, accounting)
140
+ accounting ||= {usage: adapter_result[:usage], provider_reported: !adapter_result[:usage].nil?}
141
+ append_attempt(candidate, attempt_id, started_at, :succeeded, **accounting)
121
142
  result = base_result(candidate).merge(
122
143
  status: :succeeded,
123
144
  content: adapter_result[:content] || "",
@@ -130,10 +151,10 @@ module AgentHarness
130
151
  result
131
152
  end
132
153
 
133
- def failure(candidate, attempt_id, started_at, error, partial:, usage:)
154
+ def failure(candidate, attempt_id, started_at, error, partial:, accounting:)
134
155
  status = failure_status(error, partial)
135
156
  error = error.merge(retryable: false) if partial
136
- attempts << attempt_report(candidate, attempt_id, started_at, status, error: error, usage: usage)
157
+ append_attempt(candidate, attempt_id, started_at, status, error: error, **accounting)
137
158
  result = base_result(candidate).merge(
138
159
  status: status,
139
160
  content: partial ? @partial_content.dup : "",
@@ -178,8 +199,9 @@ module AgentHarness
178
199
  def backoff
179
200
  exponent = [attempts.length - 1, 0].max
180
201
  delay = retry_config[:base_delay_seconds] * (2**exponent)
202
+ delay = [delay, @last_error[:retry_after_seconds].to_f].max
181
203
  cap = retry_config[:max_delay_seconds]
182
- remaining = cap&.positive? ? [delay, cap].min : delay
204
+ remaining = cap.nil? ? delay : [delay, cap].min
183
205
  while remaining.positive? && active?
184
206
  interval = [remaining, 0.05].min
185
207
  @sleeper.call(interval)
@@ -227,7 +249,8 @@ module AgentHarness
227
249
 
228
250
  def normalize_tool_calls(tool_calls)
229
251
  Array(tool_calls).map do |call|
230
- call.merge(id: tool_id(call[:provider_id]), status: :completed)
252
+ id = call[:provider_id].nil? ? @id_generator.call : tool_id(call[:provider_id])
253
+ call.merge(id: id, status: :completed)
231
254
  end
232
255
  end
233
256
 
@@ -265,8 +288,10 @@ module AgentHarness
265
288
  usage: aggregate_usage, error: error)
266
289
  end
267
290
 
268
- def attempt_report(candidate, attempt_id, started_at, status, error: nil, usage: nil)
269
- {
291
+ def append_attempt(candidate, attempt_id, started_at, status, error: nil, usage: nil, cost: nil,
292
+ provider_reported: false)
293
+ cost = cost.merge(priced_at: Time.now.utc) if cost
294
+ report = AttemptReport.new(
270
295
  attempt_id: attempt_id,
271
296
  request_id: request[:request_id],
272
297
  number: attempts.length + 1,
@@ -276,20 +301,22 @@ module AgentHarness
276
301
  started_at: started_at.iso8601(6),
277
302
  finished_at: Time.now.utc.iso8601(6),
278
303
  usage: usage,
279
- cost: nil,
280
- provider_reported: false,
304
+ cost: cost,
305
+ provider_reported: provider_reported,
281
306
  error: error
282
- }
307
+ ).attributes
308
+ attempts << report
309
+ emit(:attempt_completed, attempt_id:, attempt: report)
283
310
  end
284
311
 
285
312
  def aggregate_usage
286
313
  reports = attempts.filter_map { |attempt| attempt[:usage] }
287
314
  return if reports.empty?
288
315
 
289
- %i[input_tokens output_tokens total_tokens].to_h do |key|
316
+ %i[input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens total_tokens].to_h do |key|
290
317
  values = reports.filter_map { |usage| usage[key] }
291
318
  [key, values.empty? ? nil : values.sum]
292
- end
319
+ end.compact
293
320
  end
294
321
 
295
322
  def classify(error)
@@ -402,10 +429,23 @@ module AgentHarness
402
429
  end
403
430
 
404
431
  def self.payload(error, category, code, retryable)
405
- {category: category, code: code, retryable: retryable, message: safe_message(category, code)}
432
+ payload = {category: category, code: code, retryable: retryable, message: safe_message(category, code)}
433
+ retry_after = retry_after_seconds(error)
434
+ payload[:retry_after_seconds] = retry_after if retry_after
435
+ payload
406
436
  end
407
437
  private_class_method :payload
408
438
 
439
+ def self.retry_after_seconds(error)
440
+ return unless error.respond_to?(:response) && error.response
441
+
442
+ headers = error.response.respond_to?(:headers) ? error.response.headers : error.response[:response_headers]
443
+ value = headers&.find { |key, _| key.to_s.casecmp?("retry-after") }&.last
444
+ delay = Float(value, exception: false)
445
+ delay if delay&.finite? && delay >= 0
446
+ end
447
+ private_class_method :retry_after_seconds
448
+
409
449
  def self.safe_message(category, code)
410
450
  "Chat request failed (#{category}/#{code})"
411
451
  end
@@ -14,18 +14,31 @@ module AgentHarness
14
14
  anthropic: %i[anthropic_api_key anthropic_api_base],
15
15
  openai: %i[openai_api_key openai_api_base]
16
16
  }.freeze
17
+ DEFAULT_REQUEST_TIMEOUT = 300
17
18
  OPENAI_UNSUPPLIED_CONFIG = %i[openai_organization_id openai_project_id openai_use_system_role].freeze
18
19
 
19
- def call(candidate:, messages:, tools:, max_output_tokens:, temperature:, stream:, timeout:, cancellation:, &on_event)
20
- context = build_context(candidate, timeout)
21
- chat = context.chat(model: candidate[:model], provider: candidate[:provider], protocol: ruby_llm_protocol(candidate),
22
- assume_model_exists: true)
23
- configure_chat(chat, candidate, messages, tools, max_output_tokens, temperature)
24
- response = generate(chat, stream, cancellation, &on_event)
20
+ def call(candidate:, messages:, tools:, max_output_tokens:, temperature:, stream:, timeout:, cancellation:,
21
+ on_accounting: nil, prepared_chat: nil, &on_event)
22
+ provider_usage_reported = false
23
+ chat = prepared_chat || prepare(candidate:, messages:, tools:, max_output_tokens:, temperature:, timeout:,
24
+ on_accounting:, provider_usage: -> { provider_usage_reported })
25
+ response = generate(chat, stream, cancellation) do |event|
26
+ provider_usage_reported = true if event[:type] == :usage_updated
27
+ on_event&.call(event)
28
+ end
25
29
  emit_completed_tool_calls(response, &on_event) if stream
26
30
  normalize_response(response)
27
31
  end
28
32
 
33
+ def prepare(candidate:, messages:, tools:, max_output_tokens:, temperature:, timeout:, on_accounting: nil,
34
+ provider_usage: -> { false })
35
+ context = build_context(candidate, timeout, on_accounting, provider_usage)
36
+ context.chat(model: candidate[:model], provider: candidate[:provider], protocol: ruby_llm_protocol(candidate),
37
+ assume_model_exists: true).tap do |chat|
38
+ configure_chat(chat, candidate, messages, tools, max_output_tokens, temperature)
39
+ end
40
+ end
41
+
29
42
  # RubyLLM keys streamed tool-call chunks by a stream index while
30
43
  # continuation chunks carry no call id, so correlation state must live
31
44
  # between chunks. Also remembers the latest cumulative token counts so
@@ -69,7 +82,7 @@ module AgentHarness
69
82
  candidate[:protocol]
70
83
  end
71
84
 
72
- def build_context(candidate, timeout)
85
+ def build_context(candidate, timeout, on_accounting, provider_usage)
73
86
  provider = candidate[:provider].to_sym
74
87
  config_keys = PROVIDER_CONFIG[provider]
75
88
  raise RubyLLM::ConfigurationError, "Unsupported chat provider: #{provider}" unless config_keys
@@ -80,10 +93,51 @@ module AgentHarness
80
93
  config.public_send("#{config_keys[1]}=", candidate[:endpoint])
81
94
  clear_unsupplied_openai_config(config) if provider == :openai
82
95
  config.max_retries = 0
96
+ config.instrumenter = UsageInstrumenter.new(on_accounting, provider_usage) if on_accounting
83
97
  apply_timeout(config, timeout)
84
98
  end
85
99
  end
86
100
 
101
+ # Retains only accounting instrumentation; other events may contain
102
+ # request content and are intentionally discarded.
103
+ class UsageInstrumenter
104
+ def initialize(callback, provider_usage)
105
+ @callback = callback
106
+ @provider_usage = provider_usage
107
+ end
108
+
109
+ def instrument(name, payload)
110
+ @callback.call(normalize(payload)) if name == "usage.ruby_llm"
111
+ yield(payload) if block_given?
112
+ end
113
+
114
+ private
115
+
116
+ def normalize(payload)
117
+ tokens = payload.fetch(:tokens)
118
+ cost = payload.fetch(:cost)
119
+ usage = tokens.to_h
120
+ usage[:total_tokens] = tokens.input + tokens.output if tokens.input && tokens.output
121
+ {
122
+ usage: usage,
123
+ cost: cost.total.nil? ? nil : cost.to_h.merge(source: cost_source(tokens), currency: "USD"),
124
+ provider_reported: provider_reported?(payload, usage)
125
+ }
126
+ end
127
+
128
+ def provider_reported?(payload, usage)
129
+ return false if usage.empty?
130
+ return true unless payload[:status]&.to_sym == :failed
131
+ return true if @provider_usage.call
132
+
133
+ usage.values.compact.any?(&:positive?)
134
+ end
135
+
136
+ def cost_source(tokens)
137
+ tokens.reported_cost.nil? ? :estimated : :provider_reported
138
+ end
139
+ end
140
+
87
141
  def clear_unsupplied_openai_config(config)
88
142
  OPENAI_UNSUPPLIED_CONFIG.each { |key| config.public_send("#{key}=", nil) }
89
143
  end
@@ -93,8 +147,7 @@ module AgentHarness
93
147
  raise UnsupportedOptionError, "RubyLLM does not support request-local connect timeouts"
94
148
  end
95
149
 
96
- seconds = timeout&.dig(:read_seconds)
97
- config.request_timeout = seconds if seconds
150
+ config.request_timeout = timeout&.dig(:read_seconds) || DEFAULT_REQUEST_TIMEOUT
98
151
  end
99
152
 
100
153
  def configure_chat(chat, candidate, messages, tools, max_output_tokens, temperature)
@@ -262,11 +315,9 @@ module AgentHarness
262
315
  end
263
316
 
264
317
  def normalize_usage(tokens)
265
- input = tokens&.input
266
- output = tokens&.output
267
- return unless input || output
318
+ return unless tokens&.to_h&.any?
268
319
 
269
- {input_tokens: input, output_tokens: output, total_tokens: (input && output) ? input + output : nil}
320
+ tokens.to_h.merge(total_tokens: (tokens.input && tokens.output) ? tokens.input + tokens.output : nil)
270
321
  end
271
322
  end
272
323
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.41.0"
4
+ VERSION = "0.42.0"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -471,6 +471,7 @@ require_relative "agent_harness/error_taxonomy"
471
471
  require_relative "agent_harness/text_transport"
472
472
  require_relative "agent_harness/openai_compatible_transport"
473
473
  require_relative "agent_harness/conversation"
474
+ require_relative "agent_harness/api/attempt_report"
474
475
  require_relative "agent_harness/api/chat_transport"
475
476
  require_relative "agent_harness/quota_status"
476
477
  require_relative "agent_harness/authentication"
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.41.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -135,6 +135,7 @@ files:
135
135
  - json-2.18.1.gem
136
136
  - lib/agent-harness.rb
137
137
  - lib/agent_harness.rb
138
+ - lib/agent_harness/api/attempt_report.rb
138
139
  - lib/agent_harness/api/chat_transport.rb
139
140
  - lib/agent_harness/api/ruby_llm_chat_adapter.rb
140
141
  - lib/agent_harness/authentication.rb