agent-harness 0.7.3 → 0.7.4

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: 3e879475ab73c89cd1dd1a107ce769e355426cee936e95df377ec242312cec4b
4
- data.tar.gz: 492ed111e0b70703f5f55d2a448259450132515a614bc597310ddecea775a313
3
+ metadata.gz: 278785d86727fd759e55bcd8fd4fb4124a13c8f6ae818a40b2ae49bcbbb3b18f
4
+ data.tar.gz: 717338d556ef335ebf3d4e2f0fbdb4a9d92bbbe52bbb1739fcb8afaba7b0c1ac
5
5
  SHA512:
6
- metadata.gz: 02c690080d6dc6c39275c5188493c6e6a7a29303af35d1435d249ef996234235fddedb767489d429f0d98283429ec57dc8a033367aea7ca89278596ddf34d452
7
- data.tar.gz: 4a5be3565b1c35b73abc61abc2238b6a5d41416d736162624a0db14fe30e96e01029eb687e35916ecd81cefbbe2d0397113339a80ee3965dcb9b943415223745
6
+ metadata.gz: 76cd57c3875f38271390f3f7ebe29153d40924988315807d79fd85d37fdedde109e7c465a6eeeb889c858a6da53faac5cd48dc8a62862fed5d6843e73b4036a7
7
+ data.tar.gz: 6d74d1ac89feb72339a87bb08b413ee6996b0dc1b0b7cb7ff2446ac3ec12539434a00f57187e1632ec889b5fba5bab10ce20d2b91a7aaee5b21e39c837101b04
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.3"
2
+ ".": "0.7.4"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.4](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.3...agent-harness/v0.7.4) (2026-04-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 119: Claude provider leaks raw --output-format json envelope as response.output ([#120](https://github.com/viamin/agent-harness/issues/120)) ([602a5f9](https://github.com/viamin/agent-harness/commit/602a5f97e009ac59c798c7b1d7342cd43e2e8d4f))
9
+
3
10
  ## [0.7.3](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.2...agent-harness/v0.7.3) (2026-04-15)
4
11
 
5
12
 
@@ -161,8 +161,81 @@ module AgentHarness
161
161
  Base::DEFAULT_SMOKE_TEST_CONTRACT
162
162
  end
163
163
 
164
+ # Parse a raw Claude CLI --output-format=json envelope into its components.
165
+ #
166
+ # Downstream callers that capture Claude CLI stdout directly (e.g. container
167
+ # execution plans) can use this to extract the assistant text, error state,
168
+ # token usage, and structured metadata without re-implementing the parsing.
169
+ #
170
+ # @param json_string [String] raw JSON envelope from Claude CLI stdout
171
+ # @return [Hash, nil] parsed components or nil if not a valid envelope
172
+ # - :output [String] the assistant's final text (the "result" field)
173
+ # - :error [String, nil] error message if is_error was true
174
+ # - :tokens [Hash, nil] {input:, output:, total:} token counts
175
+ # - :metadata [Hash] structured metadata (cost_usd, session_id, etc.)
176
+ def parse_cli_json_envelope(json_string)
177
+ return nil if json_string.nil? || json_string.empty?
178
+
179
+ parsed = JSON.parse(json_string)
180
+ return nil unless parsed.is_a?(Hash) && parsed.key?("result")
181
+
182
+ output = parsed["result"]
183
+ error = nil
184
+
185
+ if parsed["is_error"]
186
+ error = classify_error_message(output || "Unknown Claude CLI error")
187
+ end
188
+
189
+ tokens = extract_tokens(parsed)
190
+ metadata = extract_envelope_metadata(parsed)
191
+
192
+ {output: output, error: error, tokens: tokens, metadata: metadata}
193
+ rescue JSON::ParserError
194
+ nil
195
+ end
196
+
164
197
  private
165
198
 
199
+ def classify_error_message(message)
200
+ msg_lower = message.downcase
201
+
202
+ if msg_lower.include?("rate limit") || msg_lower.include?("session limit")
203
+ "Rate limit exceeded"
204
+ elsif msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
205
+ "Model deprecated"
206
+ elsif msg_lower.include?("oauth token") || msg_lower.include?("authentication")
207
+ "Authentication error"
208
+ else
209
+ message
210
+ end
211
+ end
212
+
213
+ def extract_tokens(parsed)
214
+ usage = parsed["usage"]
215
+ return nil unless usage
216
+
217
+ input = usage["input_tokens"]
218
+ output = usage["output_tokens"]
219
+ return nil unless input || output
220
+
221
+ input ||= 0
222
+ output ||= 0
223
+
224
+ {input: input, output: output, total: input + output}
225
+ end
226
+
227
+ def extract_envelope_metadata(parsed)
228
+ meta = {}
229
+ meta[:cost_usd] = parsed["total_cost_usd"] if parsed.key?("total_cost_usd")
230
+ meta[:session_id] = parsed["session_id"] if parsed.key?("session_id")
231
+ meta[:stop_reason] = parsed["stop_reason"] if parsed.key?("stop_reason")
232
+ meta[:terminal_reason] = parsed["terminal_reason"] if parsed.key?("terminal_reason")
233
+ meta[:num_turns] = parsed["num_turns"] if parsed.key?("num_turns")
234
+ meta[:duration_ms] = parsed["duration_ms"] if parsed.key?("duration_ms")
235
+ meta[:duration_api_ms] = parsed["duration_api_ms"] if parsed.key?("duration_api_ms")
236
+ meta
237
+ end
238
+
166
239
  def validate_version!(version)
167
240
  unless version.is_a?(String) && !version.strip.empty?
168
241
  raise ArgumentError, "Invalid version: #{version.inspect}. " \
@@ -473,17 +546,24 @@ module AgentHarness
473
546
  output = result.stdout
474
547
  error = nil
475
548
  tokens = nil
549
+ metadata = {}
476
550
 
477
551
  if result.failed?
478
552
  combined = [result.stdout, result.stderr].compact.join("\n")
479
553
  error = classify_error_message(combined)
480
554
  end
481
555
 
482
- # Parse JSON output to extract result text and token usage
556
+ # Parse JSON output to extract result text, token usage, and metadata
483
557
  parsed = parse_json_output(output)
484
558
  if parsed
559
+ # Handle is_error envelopes as provider errors
560
+ if parsed["is_error"]
561
+ error ||= classify_error_message(parsed["result"] || "Unknown Claude CLI error")
562
+ end
563
+
485
564
  output = parsed["result"] || output
486
565
  tokens = extract_tokens(parsed)
566
+ metadata = extract_envelope_metadata(parsed)
487
567
  end
488
568
 
489
569
  Response.new(
@@ -493,6 +573,7 @@ module AgentHarness
493
573
  provider: self.class.provider_name,
494
574
  model: @config.model,
495
575
  tokens: tokens,
576
+ metadata: metadata,
496
577
  error: error
497
578
  )
498
579
  end
@@ -572,32 +653,18 @@ module AgentHarness
572
653
  nil
573
654
  end
574
655
 
575
- def extract_tokens(parsed)
576
- usage = parsed["usage"]
577
- return nil unless usage
578
-
579
- input = usage["input_tokens"]
580
- output = usage["output_tokens"]
581
- return nil unless input || output
582
-
583
- input ||= 0
584
- output ||= 0
656
+ # Delegate to class-level implementations so both instance and class
657
+ # methods share a single definition.
658
+ def extract_envelope_metadata(parsed)
659
+ self.class.send(:extract_envelope_metadata, parsed)
660
+ end
585
661
 
586
- {input: input, output: output, total: input + output}
662
+ def extract_tokens(parsed)
663
+ self.class.send(:extract_tokens, parsed)
587
664
  end
588
665
 
589
666
  def classify_error_message(message)
590
- msg_lower = message.downcase
591
-
592
- if msg_lower.include?("rate limit") || msg_lower.include?("session limit")
593
- "Rate limit exceeded"
594
- elsif msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
595
- "Model deprecated"
596
- elsif msg_lower.include?("oauth token") || msg_lower.include?("authentication")
597
- "Authentication error"
598
- else
599
- message
600
- end
667
+ self.class.send(:classify_error_message, message)
601
668
  end
602
669
 
603
670
  def parse_claude_mcp_output(output)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.7.3"
4
+ VERSION = "0.7.4"
5
5
  end
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.7.3
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan