agent-harness 0.11.3 → 0.12.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 +7 -0
- data/lib/agent_harness/providers/codex.rb +169 -2
- 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: 9e1a243bc20db360b93c22248fd0b35864a995dd6a78def3fdca291d5e39809d
|
|
4
|
+
data.tar.gz: 591bc4baf70eeb598b4c7a6736b4c0090b7108c6f34628b75a1b5b68eb991805
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1028da8c56be8d3f7e948dd23bfd25fbfae3515d1f678fe5c9b5172f5f5a82dbd0fad959ef94d2681619fdf44a6d509dcfc925bc20fb2d3e4e2c92f44ea19d9a
|
|
7
|
+
data.tar.gz: f8ed1ca7bccfa683eee9303a977920d177a62832b48d937b01c107407890ea60b17680030cf4bc8ca3cd3bdf495def3d90518da89943d55306239ee15869c0c2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.3...agent-harness/v0.12.0) (2026-05-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
|
|
3
10
|
## [0.11.3](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.2...agent-harness/v0.11.3) (2026-04-28)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -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
|