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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/agent_harness/providers/anthropic.rb +90 -23
- data/lib/agent_harness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 278785d86727fd759e55bcd8fd4fb4124a13c8f6ae818a40b2ae49bcbbb3b18f
|
|
4
|
+
data.tar.gz: 717338d556ef335ebf3d4e2f0fbdb4a9d92bbbe52bbb1739fcb8afaba7b0c1ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76cd57c3875f38271390f3f7ebe29153d40924988315807d79fd85d37fdedde109e7c465a6eeeb889c858a6da53faac5cd48dc8a62862fed5d6843e73b4036a7
|
|
7
|
+
data.tar.gz: 6d74d1ac89feb72339a87bb08b413ee6996b0dc1b0b7cb7ff2446ac3ec12539434a00f57187e1632ec889b5fba5bab10ce20d2b91a7aaee5b21e39c837101b04
|
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
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|