agent-harness 0.11.3 → 0.13.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/lib/agent_harness/providers/aider.rb +27 -0
- data/lib/agent_harness/providers/base.rb +23 -0
- data/lib/agent_harness/providers/codex.rb +169 -2
- data/lib/agent_harness/providers/github_copilot.rb +27 -0
- 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: 5248389a9a7500880e23672e9daad822c4c2038f5424ed2b278ab7c6276cf9a5
|
|
4
|
+
data.tar.gz: 806895feee0bd65477498453d32de90985849a70476560dabd7f0330995db6a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48652585f74b61a2a70a4c118a23cb93977dca88e743ec76cd6c12802cb005cc24c703f25b0994a8b5d423f59114be93c8ab040f957191ac782d6260f6d5ffde
|
|
7
|
+
data.tar.gz: f37ece29dc1dbd311713abd42aeb99363f7bf41ede1e31185fea16c41a312ace62d8247fb3e6034a38ac519797c131000480832921376141a2fe88bbc2b6e822
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.12.0...agent-harness/v0.13.0) (2026-05-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Expose public parse_container_output method on provider interface ([#187](https://github.com/viamin/agent-harness/issues/187)) ([ecdb7ba](https://github.com/viamin/agent-harness/commit/ecdb7bac56e47cf75e1379508cca64a9c7a0ffff))
|
|
9
|
+
|
|
10
|
+
## [0.12.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.3...agent-harness/v0.12.0) (2026-05-01)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* streaming JSONL event parser for real-time Codex progress tracking ([#184](https://github.com/viamin/agent-harness/issues/184)) ([4905539](https://github.com/viamin/agent-harness/commit/490553992904f39e52028b2140ab99755aad1fb1))
|
|
16
|
+
|
|
3
17
|
## [0.11.3](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.2...agent-harness/v0.11.3) (2026-04-28)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -263,6 +263,33 @@ module AgentHarness
|
|
|
263
263
|
cleanup_llm_history_file!(llm_history_path)
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
+
# Parse raw container output into a Response.
|
|
267
|
+
#
|
|
268
|
+
# Overrides the base implementation to support the
|
|
269
|
+
# +llm_history_path+ option for token usage extraction from
|
|
270
|
+
# Aider's LLM history file.
|
|
271
|
+
#
|
|
272
|
+
# @param stdout [String] captured standard output
|
|
273
|
+
# @param stderr [String] captured standard error
|
|
274
|
+
# @param exit_code [Integer] process exit code
|
|
275
|
+
# @param duration [Float] execution duration in seconds
|
|
276
|
+
# @param options [Hash] additional options
|
|
277
|
+
# @option options [String, nil] :llm_history_path path to LLM history file
|
|
278
|
+
# @return [Response] parsed response
|
|
279
|
+
def parse_container_output(stdout:, stderr: "", exit_code: 0, duration: 0.0, **options)
|
|
280
|
+
result = CommandExecutor::Result.new(
|
|
281
|
+
stdout: stdout,
|
|
282
|
+
stderr: stderr,
|
|
283
|
+
exit_code: exit_code,
|
|
284
|
+
duration: duration
|
|
285
|
+
)
|
|
286
|
+
parse_response(
|
|
287
|
+
result,
|
|
288
|
+
duration: duration,
|
|
289
|
+
llm_history_path: options[:llm_history_path]
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
266
293
|
protected
|
|
267
294
|
|
|
268
295
|
def build_command(prompt, options)
|
|
@@ -265,6 +265,29 @@ module AgentHarness
|
|
|
265
265
|
handle_error(e, prompt: (last_msg&.dig(:content) || last_msg&.dig("content")).to_s, options: options)
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
# Parse raw container output into a Response.
|
|
269
|
+
#
|
|
270
|
+
# This is the public interface for parsing CLI output captured from
|
|
271
|
+
# external execution (e.g. Docker containers) without going through
|
|
272
|
+
# send_message. It accepts the same data a CommandExecutor::Result
|
|
273
|
+
# holds and returns an AgentHarness::Response.
|
|
274
|
+
#
|
|
275
|
+
# @param stdout [String] captured standard output
|
|
276
|
+
# @param stderr [String] captured standard error
|
|
277
|
+
# @param exit_code [Integer] process exit code
|
|
278
|
+
# @param duration [Float] execution duration in seconds
|
|
279
|
+
# @param options [Hash] additional provider-specific options
|
|
280
|
+
# @return [Response] parsed response
|
|
281
|
+
def parse_container_output(stdout:, stderr: "", exit_code: 0, duration: 0.0, **options)
|
|
282
|
+
result = CommandExecutor::Result.new(
|
|
283
|
+
stdout: stdout,
|
|
284
|
+
stderr: stderr,
|
|
285
|
+
exit_code: exit_code,
|
|
286
|
+
duration: duration
|
|
287
|
+
)
|
|
288
|
+
parse_response(result, duration: duration)
|
|
289
|
+
end
|
|
290
|
+
|
|
268
291
|
# Provider name for display
|
|
269
292
|
#
|
|
270
293
|
# @return [String] display name
|
|
@@ -11,6 +11,11 @@ module AgentHarness
|
|
|
11
11
|
include RateLimitResetParsing
|
|
12
12
|
include McpConfigFileSupport
|
|
13
13
|
|
|
14
|
+
StreamingEvent = Struct.new(
|
|
15
|
+
:type, :turn, :tokens, :error_message, :tool_name, :raw_event,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
|
|
14
19
|
SUPPORTED_CLI_VERSION = "0.116.0"
|
|
15
20
|
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.117.0").freeze
|
|
16
21
|
OAUTH_REFRESH_FAILURE_PATTERNS = [
|
|
@@ -142,15 +147,31 @@ module AgentHarness
|
|
|
142
147
|
end
|
|
143
148
|
|
|
144
149
|
def parse_cli_jsonl_transcript(raw_output, max_events: nil)
|
|
145
|
-
return
|
|
150
|
+
return parser_instance.send(:parse_jsonl_output, "") if max_events && max_events <= 0
|
|
146
151
|
|
|
147
152
|
output = max_events ? tail_nonempty_lines(raw_output, limit: max_events).join("\n") : raw_output
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
parser_instance.send(:parse_jsonl_output, output)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Parse a single Codex JSONL event as it arrives on stdout and classify it
|
|
158
|
+
# for real-time progress tracking. Returns nil for malformed JSON, scalar
|
|
159
|
+
# JSON values, plain-text output, or unsupported event types.
|
|
160
|
+
def parse_streaming_event(line)
|
|
161
|
+
event = JSON.parse(line.to_s)
|
|
162
|
+
return unless event.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
parser_instance.send(:build_streaming_event, event)
|
|
165
|
+
rescue JSON::ParserError, TypeError
|
|
166
|
+
nil
|
|
150
167
|
end
|
|
151
168
|
|
|
152
169
|
private
|
|
153
170
|
|
|
171
|
+
def parser_instance
|
|
172
|
+
@parser_instance ||= allocate.freeze
|
|
173
|
+
end
|
|
174
|
+
|
|
154
175
|
def tail_nonempty_lines(text, limit:)
|
|
155
176
|
return [] if limit <= 0
|
|
156
177
|
|
|
@@ -507,6 +528,152 @@ module AgentHarness
|
|
|
507
528
|
|
|
508
529
|
private
|
|
509
530
|
|
|
531
|
+
def build_streaming_event(event)
|
|
532
|
+
raw_event, payload, dispatch_type = unwrap_streaming_event(event)
|
|
533
|
+
return unless payload.is_a?(Hash)
|
|
534
|
+
|
|
535
|
+
case dispatch_type
|
|
536
|
+
when "message.delta", "agent_message_delta"
|
|
537
|
+
build_progress_streaming_event(raw_event, payload)
|
|
538
|
+
when "turn.completed", "task_complete", "turn_complete"
|
|
539
|
+
build_turn_complete_streaming_event(raw_event, payload)
|
|
540
|
+
when "turn.failed"
|
|
541
|
+
build_error_streaming_event(raw_event, payload)
|
|
542
|
+
when "item.completed", "response_item", "agent_message"
|
|
543
|
+
build_item_streaming_event(raw_event, payload)
|
|
544
|
+
when "token_count"
|
|
545
|
+
build_token_usage_streaming_event(raw_event, payload)
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def unwrap_streaming_event(event)
|
|
550
|
+
event_type = event["type"]
|
|
551
|
+
|
|
552
|
+
if event_type == "event_msg"
|
|
553
|
+
payload = event["payload"]
|
|
554
|
+
[event, payload, payload.is_a?(Hash) ? payload["type"] : nil]
|
|
555
|
+
elsif event_type == "response_item"
|
|
556
|
+
# Preserve the original "response_item" dispatch type so
|
|
557
|
+
# build_streaming_event routes to build_item_streaming_event
|
|
558
|
+
# even after unwrapping the inner payload.
|
|
559
|
+
[event, event["payload"], "response_item"]
|
|
560
|
+
else
|
|
561
|
+
[event, event, event_type]
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def build_progress_streaming_event(raw_event, payload)
|
|
566
|
+
return unless progress_payload?(payload)
|
|
567
|
+
|
|
568
|
+
StreamingEvent.new(
|
|
569
|
+
type: :progress,
|
|
570
|
+
turn: extract_streaming_turn(payload),
|
|
571
|
+
raw_event: raw_event
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def build_turn_complete_streaming_event(raw_event, payload)
|
|
576
|
+
StreamingEvent.new(
|
|
577
|
+
type: :turn_complete,
|
|
578
|
+
turn: extract_streaming_turn(payload),
|
|
579
|
+
tokens: compact_streaming_tokens(build_token_usage(payload["usage"])),
|
|
580
|
+
raw_event: raw_event
|
|
581
|
+
)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def build_error_streaming_event(raw_event, payload)
|
|
585
|
+
StreamingEvent.new(
|
|
586
|
+
type: :error,
|
|
587
|
+
turn: extract_streaming_turn(payload),
|
|
588
|
+
tokens: compact_streaming_tokens(build_token_usage(payload["usage"])),
|
|
589
|
+
error_message: extract_error_message(payload),
|
|
590
|
+
raw_event: raw_event
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def build_item_streaming_event(raw_event, payload)
|
|
595
|
+
item = payload["item"].is_a?(Hash) ? payload["item"] : payload
|
|
596
|
+
|
|
597
|
+
if tool_use_payload?(item)
|
|
598
|
+
return StreamingEvent.new(
|
|
599
|
+
type: :tool_use,
|
|
600
|
+
turn: extract_streaming_turn(payload),
|
|
601
|
+
tool_name: extract_tool_name(item),
|
|
602
|
+
raw_event: raw_event
|
|
603
|
+
)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
return unless assistant_message_item?(item) || response_item_assistant_payload?(item) || wrapped_assistant_payload?(item)
|
|
607
|
+
|
|
608
|
+
StreamingEvent.new(
|
|
609
|
+
type: :progress,
|
|
610
|
+
turn: extract_streaming_turn(payload),
|
|
611
|
+
raw_event: raw_event
|
|
612
|
+
)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def build_token_usage_streaming_event(raw_event, payload)
|
|
616
|
+
wrapped_token_usage = extract_wrapped_tokens(payload["info"])
|
|
617
|
+
usage = wrapped_token_usage&.fetch(:last, nil) || wrapped_token_usage&.fetch(:total, nil)
|
|
618
|
+
return unless usage
|
|
619
|
+
|
|
620
|
+
StreamingEvent.new(
|
|
621
|
+
type: :token_usage,
|
|
622
|
+
turn: extract_streaming_turn(payload),
|
|
623
|
+
tokens: compact_streaming_tokens(usage),
|
|
624
|
+
raw_event: raw_event
|
|
625
|
+
)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def progress_payload?(payload)
|
|
629
|
+
case payload["type"]
|
|
630
|
+
when "message.delta"
|
|
631
|
+
payload["delta"].is_a?(Hash)
|
|
632
|
+
when "agent_message_delta"
|
|
633
|
+
wrapped_assistant_payload?(payload)
|
|
634
|
+
else
|
|
635
|
+
false
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def tool_use_payload?(item)
|
|
640
|
+
item.is_a?(Hash) && item["type"] == "tool_call"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def extract_tool_name(item)
|
|
644
|
+
item["tool_name"] || item["name"] || item.dig("function", "name") || item.dig("call", "name")
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def extract_streaming_turn(payload)
|
|
648
|
+
value = payload["turn"] || payload["turn_id"] || payload["turn_index"] || payload.dig("context", "turn")
|
|
649
|
+
return value if value.is_a?(Integer)
|
|
650
|
+
|
|
651
|
+
value.to_i if value.is_a?(String) && /\A\d+\z/.match?(value.strip)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def compact_streaming_tokens(usage)
|
|
655
|
+
return unless usage
|
|
656
|
+
|
|
657
|
+
{
|
|
658
|
+
input: usage[:input],
|
|
659
|
+
output: usage[:output],
|
|
660
|
+
total: usage[:total]
|
|
661
|
+
}
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def extract_error_message(payload)
|
|
665
|
+
error = payload["error"]
|
|
666
|
+
|
|
667
|
+
case error
|
|
668
|
+
when String
|
|
669
|
+
error
|
|
670
|
+
when Hash
|
|
671
|
+
error["message"] || error["error"] || error["detail"]
|
|
672
|
+
else
|
|
673
|
+
payload["message"]
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
510
677
|
def escape_toml_string(val)
|
|
511
678
|
val.to_s.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("\n") { "\\n" }
|
|
512
679
|
end
|
|
@@ -345,6 +345,33 @@ module AgentHarness
|
|
|
345
345
|
handle_error(e, prompt: prompt, options: options)
|
|
346
346
|
end
|
|
347
347
|
|
|
348
|
+
# Parse raw container output into a Response.
|
|
349
|
+
#
|
|
350
|
+
# Overrides the base implementation to support the
|
|
351
|
+
# +json_output_requested+ option, which controls whether JSONL
|
|
352
|
+
# output is parsed for token extraction.
|
|
353
|
+
#
|
|
354
|
+
# @param stdout [String] captured standard output
|
|
355
|
+
# @param stderr [String] captured standard error
|
|
356
|
+
# @param exit_code [Integer] process exit code
|
|
357
|
+
# @param duration [Float] execution duration in seconds
|
|
358
|
+
# @param options [Hash] additional options
|
|
359
|
+
# @option options [Boolean] :json_output_requested whether to parse JSONL output
|
|
360
|
+
# @return [Response] parsed response
|
|
361
|
+
def parse_container_output(stdout:, stderr: "", exit_code: 0, duration: 0.0, **options)
|
|
362
|
+
result = CommandExecutor::Result.new(
|
|
363
|
+
stdout: stdout,
|
|
364
|
+
stderr: stderr,
|
|
365
|
+
exit_code: exit_code,
|
|
366
|
+
duration: duration
|
|
367
|
+
)
|
|
368
|
+
parse_response(
|
|
369
|
+
result,
|
|
370
|
+
duration: duration,
|
|
371
|
+
json_output_requested: options.fetch(:json_output_requested, false)
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
|
|
348
375
|
protected
|
|
349
376
|
|
|
350
377
|
def build_command(prompt, options)
|