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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a00dc60851ef15dfa731370add2fb28ac8af98975d61194801afb8a34afc5623
4
- data.tar.gz: 3422826728017a83203ab285d0e733b2958dd77bf8832c9c57dc16eead4e2a5d
3
+ metadata.gz: 9e1a243bc20db360b93c22248fd0b35864a995dd6a78def3fdca291d5e39809d
4
+ data.tar.gz: 591bc4baf70eeb598b4c7a6736b4c0090b7108c6f34628b75a1b5b68eb991805
5
5
  SHA512:
6
- metadata.gz: 32065b807ea92963f0694f28517ca0ad17fe860ac73da96d96a1b51090450cad1324f99bd761f28f55b915c0ca14d59731e10166a4ef7f206b2887a5fdfb1a81
7
- data.tar.gz: 7d4d571e6b98f2769e05ab637938cc41f7ea2ee1bbe2b73b8a8fed9a6c4d6c1a85aca58cf38578b0f81b983818e3df98251bc1e59f0d14e08d69f486429a8ee1
6
+ metadata.gz: 1028da8c56be8d3f7e948dd23bfd25fbfae3515d1f678fe5c9b5172f5f5a82dbd0fad959ef94d2681619fdf44a6d509dcfc925bc20fb2d3e4e2c92f44ea19d9a
7
+ data.tar.gz: f8ed1ca7bccfa683eee9303a977920d177a62832b48d937b01c107407890ea60b17680030cf4bc8ca3cd3bdf495def3d90518da89943d55306239ee15869c0c2
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.11.3"
2
+ ".": "0.12.0"
3
3
  }
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 new.send(:parse_jsonl_output, "") if max_events && max_events <= 0
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
- new.send(:parse_jsonl_output, output)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.11.3"
4
+ VERSION = "0.12.0"
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.11.3
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan