agent-harness 0.7.0 → 0.7.1
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 +9 -0
- data/lib/agent_harness/providers/codex.rb +775 -6
- 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: 840999010c09f5e1b70d3dd0a1631cf76e15a13738954cb2f259149f7e0df9c3
|
|
4
|
+
data.tar.gz: 79f321a55d661a7f1a018372b8fea6b1c0f55ce659bfeb62a4937c2fd5852976
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2092406f7f5f75623eea7e3b4bc3c78c9fd7ffcd6f85b1e90a2e20bdc59f4b5f4bab5d7e9e8dee7fdf8772881e311c02cec7e555ce797c01b1ec7c3f482e023
|
|
7
|
+
data.tar.gz: 3670198c4053fb94c3ec4e990cc4649b19977ccf5d7a5a6b3a95f1acb077f53c50055cd57098eda54b0fa1afaab0d1edff3429d4d0a55c3abf8137c4d846cda2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.0...agent-harness/v0.7.1) (2026-04-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **codex:** address remaining json output review feedback ([505e068](https://github.com/viamin/agent-harness/commit/505e068d63fbc5590112ba00faee0d1c62d997e3))
|
|
9
|
+
* **codex:** address review feedback for token usage extraction ([398940e](https://github.com/viamin/agent-harness/commit/398940ecb356ec9e6978d42244ae26295823bb89))
|
|
10
|
+
* **codex:** preserve output-last-message flag values ([7480778](https://github.com/viamin/agent-harness/commit/7480778d6a5d7b9227eec20889bc642eb399d1b5))
|
|
11
|
+
|
|
3
12
|
## [0.7.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.6.0...agent-harness/v0.7.0) (2026-04-13)
|
|
4
13
|
|
|
5
14
|
|
|
@@ -174,7 +174,7 @@ module AgentHarness
|
|
|
174
174
|
def execution_semantics
|
|
175
175
|
{
|
|
176
176
|
prompt_delivery: :arg,
|
|
177
|
-
output_format: :
|
|
177
|
+
output_format: :json,
|
|
178
178
|
sandbox_aware: true,
|
|
179
179
|
uses_subcommand: true,
|
|
180
180
|
non_interactive_flag: nil,
|
|
@@ -275,15 +275,49 @@ module AgentHarness
|
|
|
275
275
|
protected
|
|
276
276
|
|
|
277
277
|
def parse_response(result, duration:)
|
|
278
|
-
|
|
278
|
+
output = result.stdout
|
|
279
|
+
error = nil
|
|
280
|
+
tokens = nil
|
|
281
|
+
legitimate = execution_semantics[:legitimate_exit_codes] || [0]
|
|
282
|
+
|
|
283
|
+
unless legitimate.include?(result.exit_code)
|
|
284
|
+
combined = [result.stderr, result.stdout]
|
|
285
|
+
.map { |stream| stream.to_s.strip }
|
|
286
|
+
.reject(&:empty?)
|
|
287
|
+
.join("\n")
|
|
288
|
+
error = combined unless combined.empty?
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
parsed = parse_jsonl_output(output)
|
|
292
|
+
if parsed
|
|
293
|
+
output = parsed[:text].nil? ? output : parsed[:text]
|
|
294
|
+
tokens = parsed[:tokens]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
response = Response.new(
|
|
298
|
+
output: output,
|
|
299
|
+
exit_code: result.exit_code,
|
|
300
|
+
duration: duration,
|
|
301
|
+
provider: self.class.provider_name,
|
|
302
|
+
model: @config.model,
|
|
303
|
+
tokens: tokens,
|
|
304
|
+
metadata: {
|
|
305
|
+
legitimate_exit_codes: legitimate
|
|
306
|
+
},
|
|
307
|
+
error: error
|
|
308
|
+
)
|
|
279
309
|
|
|
280
310
|
if response.success? && sandbox_failure_detected?(result.stderr)
|
|
281
311
|
return Response.new(
|
|
282
|
-
output:
|
|
312
|
+
output: output,
|
|
283
313
|
exit_code: 1,
|
|
284
314
|
duration: duration,
|
|
285
315
|
provider: self.class.provider_name,
|
|
286
316
|
model: @config.model,
|
|
317
|
+
tokens: tokens,
|
|
318
|
+
metadata: {
|
|
319
|
+
legitimate_exit_codes: legitimate
|
|
320
|
+
},
|
|
287
321
|
error: "Sandbox failure detected: #{result.stderr.strip}"
|
|
288
322
|
)
|
|
289
323
|
end
|
|
@@ -292,8 +326,9 @@ module AgentHarness
|
|
|
292
326
|
end
|
|
293
327
|
|
|
294
328
|
def build_command(prompt, options)
|
|
295
|
-
cmd = [self.class.binary_name, "exec"]
|
|
329
|
+
cmd = [self.class.binary_name, "exec", "--json"]
|
|
296
330
|
externally_sandboxed = externally_sandboxed?(options)
|
|
331
|
+
runtime = options[:provider_runtime]
|
|
297
332
|
|
|
298
333
|
# When externally_sandboxed is set, use --dangerously-bypass-approvals-and-sandbox
|
|
299
334
|
# instead of --full-auto. In the Codex CLI, full_auto is checked first and
|
|
@@ -324,8 +359,6 @@ module AgentHarness
|
|
|
324
359
|
if options[:session]
|
|
325
360
|
cmd += session_flags(options[:session])
|
|
326
361
|
end
|
|
327
|
-
|
|
328
|
-
runtime = options[:provider_runtime]
|
|
329
362
|
if runtime
|
|
330
363
|
cmd += ["--model", runtime.model] if runtime.model
|
|
331
364
|
runtime_flags = runtime.flags
|
|
@@ -354,6 +387,742 @@ module AgentHarness
|
|
|
354
387
|
|
|
355
388
|
private
|
|
356
389
|
|
|
390
|
+
def parse_jsonl_output(raw_output)
|
|
391
|
+
return nil if raw_output.nil? || raw_output.strip.empty?
|
|
392
|
+
|
|
393
|
+
latest_completed_parts = []
|
|
394
|
+
current_turn_parts = []
|
|
395
|
+
total_input = 0
|
|
396
|
+
total_output = 0
|
|
397
|
+
total_tokens = 0
|
|
398
|
+
has_usage = false
|
|
399
|
+
saw_assistant_output = false
|
|
400
|
+
pending_turn_usage = nil
|
|
401
|
+
pending_turn_usage_source = nil
|
|
402
|
+
pending_wrapped_output_parts = nil
|
|
403
|
+
pending_wrapped_same_turn_finalization = false
|
|
404
|
+
turn_completed = false
|
|
405
|
+
current_turn_finalized_output = false
|
|
406
|
+
|
|
407
|
+
commit_pending_turn = lambda do
|
|
408
|
+
next unless pending_turn_usage
|
|
409
|
+
|
|
410
|
+
total_input += pending_turn_usage[:input]
|
|
411
|
+
total_output += pending_turn_usage[:output]
|
|
412
|
+
total_tokens += pending_turn_usage[:total]
|
|
413
|
+
pending_turn_usage = nil
|
|
414
|
+
pending_turn_usage_source = nil
|
|
415
|
+
pending_wrapped_output_parts = nil
|
|
416
|
+
pending_wrapped_same_turn_finalization = false
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
start_new_turn = lambda do
|
|
420
|
+
next unless turn_completed
|
|
421
|
+
|
|
422
|
+
commit_pending_turn.call
|
|
423
|
+
turn_completed = false
|
|
424
|
+
current_turn_finalized_output = false
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
start_new_finalized_turn = lambda do
|
|
428
|
+
start_new_turn.call
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
start_new_streaming_turn = lambda do
|
|
432
|
+
start_new_turn.call
|
|
433
|
+
next unless pending_turn_usage_source == :wrapped && pending_turn_usage && current_turn_finalized_output
|
|
434
|
+
|
|
435
|
+
latest_completed_parts = current_turn_parts.dup
|
|
436
|
+
commit_pending_turn.call
|
|
437
|
+
current_turn_parts = []
|
|
438
|
+
current_turn_finalized_output = false
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
replace_current_turn_parts = lambda do |parts|
|
|
442
|
+
next if parts.nil?
|
|
443
|
+
|
|
444
|
+
current_turn_parts = parts
|
|
445
|
+
saw_assistant_output = true
|
|
446
|
+
current_turn_finalized_output = true
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
finalize_current_turn = lambda do
|
|
450
|
+
latest_completed_parts = current_turn_parts.dup
|
|
451
|
+
current_turn_parts = []
|
|
452
|
+
turn_completed = true
|
|
453
|
+
current_turn_finalized_output = false
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
finalize_pending_wrapped_turn = lambda do
|
|
457
|
+
next unless pending_turn_usage_source == :wrapped && pending_turn_usage
|
|
458
|
+
|
|
459
|
+
wrapped_output_parts = pending_wrapped_output_parts || current_turn_parts
|
|
460
|
+
latest_completed_parts = wrapped_output_parts.dup
|
|
461
|
+
current_turn_parts = [] if current_turn_parts.equal?(wrapped_output_parts)
|
|
462
|
+
commit_pending_turn.call
|
|
463
|
+
turn_completed = false
|
|
464
|
+
current_turn_finalized_output = false
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
fail_current_turn = lambda do
|
|
468
|
+
latest_completed_parts = []
|
|
469
|
+
current_turn_parts = []
|
|
470
|
+
turn_completed = true
|
|
471
|
+
current_turn_finalized_output = false
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
process_event = lambda do |event|
|
|
475
|
+
next unless event.is_a?(Hash)
|
|
476
|
+
|
|
477
|
+
type = event["type"]
|
|
478
|
+
|
|
479
|
+
case type
|
|
480
|
+
when "message.delta"
|
|
481
|
+
start_new_streaming_turn.call
|
|
482
|
+
appended = append_delta_text(current_turn_parts, event["delta"])
|
|
483
|
+
current_turn_finalized_output = false if appended
|
|
484
|
+
saw_assistant_output ||= appended
|
|
485
|
+
when "agent_message_delta"
|
|
486
|
+
next unless wrapped_assistant_payload?(event)
|
|
487
|
+
|
|
488
|
+
start_new_streaming_turn.call
|
|
489
|
+
appended = append_wrapped_delta_text(current_turn_parts, event)
|
|
490
|
+
current_turn_finalized_output = false if appended
|
|
491
|
+
saw_assistant_output ||= appended
|
|
492
|
+
when "agent_message"
|
|
493
|
+
next unless wrapped_assistant_payload?(event)
|
|
494
|
+
|
|
495
|
+
wrapped_same_turn_finalization =
|
|
496
|
+
pending_turn_usage_source == :wrapped &&
|
|
497
|
+
pending_turn_usage &&
|
|
498
|
+
(
|
|
499
|
+
!current_turn_finalized_output ||
|
|
500
|
+
pending_wrapped_same_turn_finalization
|
|
501
|
+
)
|
|
502
|
+
start_new_turn.call
|
|
503
|
+
replace_current_turn_parts.call(extract_message_content_parts(event))
|
|
504
|
+
pending_wrapped_same_turn_finalization = wrapped_same_turn_finalization
|
|
505
|
+
when "task_complete", "turn_complete"
|
|
506
|
+
completion_parts = extract_task_complete_parts(event)
|
|
507
|
+
next if completion_parts.nil?
|
|
508
|
+
|
|
509
|
+
wrapped_same_turn_finalization =
|
|
510
|
+
pending_turn_usage_source == :wrapped &&
|
|
511
|
+
pending_turn_usage &&
|
|
512
|
+
(
|
|
513
|
+
!current_turn_finalized_output ||
|
|
514
|
+
pending_wrapped_same_turn_finalization
|
|
515
|
+
)
|
|
516
|
+
start_new_turn.call
|
|
517
|
+
replace_current_turn_parts.call(completion_parts)
|
|
518
|
+
pending_wrapped_same_turn_finalization = wrapped_same_turn_finalization
|
|
519
|
+
when "item.completed"
|
|
520
|
+
item = event["item"]
|
|
521
|
+
next unless item.is_a?(Hash)
|
|
522
|
+
next unless assistant_message_item?(item)
|
|
523
|
+
|
|
524
|
+
start_new_finalized_turn.call
|
|
525
|
+
replace_current_turn_parts.call(extract_message_content_parts(item))
|
|
526
|
+
pending_wrapped_same_turn_finalization =
|
|
527
|
+
pending_turn_usage_source == :wrapped && pending_turn_usage
|
|
528
|
+
when "turn.completed"
|
|
529
|
+
turn_usage = build_token_usage(event["usage"])
|
|
530
|
+
result = event["result"]
|
|
531
|
+
wrapped_completion_without_new_output =
|
|
532
|
+
pending_turn_usage_source == :wrapped &&
|
|
533
|
+
pending_turn_usage &&
|
|
534
|
+
!result.is_a?(String) &&
|
|
535
|
+
(turn_usage.nil? || current_turn_parts.empty? || current_turn_parts.equal?(pending_wrapped_output_parts))
|
|
536
|
+
|
|
537
|
+
if wrapped_completion_without_new_output
|
|
538
|
+
if pending_wrapped_output_parts && !current_turn_parts.empty? && !current_turn_parts.equal?(pending_wrapped_output_parts)
|
|
539
|
+
commit_pending_turn.call
|
|
540
|
+
finalize_current_turn.call
|
|
541
|
+
if turn_usage
|
|
542
|
+
has_usage = true
|
|
543
|
+
pending_turn_usage = turn_usage
|
|
544
|
+
pending_turn_usage_source = :turn_completed
|
|
545
|
+
pending_wrapped_same_turn_finalization = false
|
|
546
|
+
end
|
|
547
|
+
next
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
wrapped_output_parts = pending_wrapped_output_parts || current_turn_parts
|
|
551
|
+
latest_completed_parts = wrapped_output_parts.dup
|
|
552
|
+
current_turn_parts = [] if current_turn_parts.equal?(wrapped_output_parts)
|
|
553
|
+
commit_pending_turn.call
|
|
554
|
+
if turn_usage
|
|
555
|
+
has_usage = true
|
|
556
|
+
total_input += turn_usage[:input]
|
|
557
|
+
total_output += turn_usage[:output]
|
|
558
|
+
total_tokens += turn_usage[:total]
|
|
559
|
+
end
|
|
560
|
+
turn_completed = true
|
|
561
|
+
current_turn_finalized_output = false
|
|
562
|
+
next
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
same_streaming_wrapped_turn =
|
|
566
|
+
pending_turn_usage_source == :wrapped &&
|
|
567
|
+
pending_wrapped_output_parts&.equal?(current_turn_parts) &&
|
|
568
|
+
!current_turn_finalized_output
|
|
569
|
+
same_wrapped_turn = pending_turn_usage_source == :wrapped &&
|
|
570
|
+
same_turn_usage?(pending_turn_usage, turn_usage) &&
|
|
571
|
+
(
|
|
572
|
+
same_turn_output?(current_turn_parts, current_turn_finalized_output, result) ||
|
|
573
|
+
same_streaming_wrapped_turn
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
finalize_pending_wrapped_turn.call unless same_wrapped_turn
|
|
577
|
+
|
|
578
|
+
if turn_completed && !same_wrapped_turn
|
|
579
|
+
commit_pending_turn.call
|
|
580
|
+
turn_completed = false
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
if turn_usage
|
|
584
|
+
has_usage = true
|
|
585
|
+
turn_usage = merge_same_turn_usage(pending_turn_usage, turn_usage) if same_wrapped_turn
|
|
586
|
+
pending_turn_usage = turn_usage
|
|
587
|
+
pending_turn_usage_source = :turn_completed
|
|
588
|
+
pending_wrapped_same_turn_finalization = false
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
if result.is_a?(String)
|
|
592
|
+
current_turn_parts = [result]
|
|
593
|
+
saw_assistant_output = true
|
|
594
|
+
current_turn_finalized_output = true
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
finalize_current_turn.call
|
|
598
|
+
when "turn.failed"
|
|
599
|
+
turn_usage = build_token_usage(event["usage"])
|
|
600
|
+
same_streaming_wrapped_turn =
|
|
601
|
+
pending_turn_usage_source == :wrapped &&
|
|
602
|
+
pending_wrapped_output_parts&.equal?(current_turn_parts) &&
|
|
603
|
+
!current_turn_finalized_output
|
|
604
|
+
same_finalized_wrapped_turn =
|
|
605
|
+
pending_turn_usage_source == :wrapped &&
|
|
606
|
+
pending_wrapped_same_turn_finalization &&
|
|
607
|
+
current_turn_finalized_output
|
|
608
|
+
same_wrapped_turn = pending_turn_usage_source == :wrapped &&
|
|
609
|
+
same_turn_usage?(pending_turn_usage, turn_usage) &&
|
|
610
|
+
(
|
|
611
|
+
pending_wrapped_output_parts&.equal?(current_turn_parts) ||
|
|
612
|
+
same_streaming_wrapped_turn ||
|
|
613
|
+
same_finalized_wrapped_turn
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
finalize_pending_wrapped_turn.call unless same_wrapped_turn
|
|
617
|
+
|
|
618
|
+
if turn_completed && !same_wrapped_turn
|
|
619
|
+
commit_pending_turn.call
|
|
620
|
+
turn_completed = false
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
if turn_usage
|
|
624
|
+
has_usage = true
|
|
625
|
+
turn_usage = merge_same_turn_usage(pending_turn_usage, turn_usage) if same_wrapped_turn
|
|
626
|
+
pending_turn_usage = turn_usage
|
|
627
|
+
pending_turn_usage_source = :turn_completed
|
|
628
|
+
pending_wrapped_same_turn_finalization = false
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
fail_current_turn.call
|
|
632
|
+
when "event_msg"
|
|
633
|
+
payload = event["payload"]
|
|
634
|
+
next unless payload.is_a?(Hash)
|
|
635
|
+
|
|
636
|
+
case payload["type"]
|
|
637
|
+
when "agent_message_delta"
|
|
638
|
+
next unless wrapped_assistant_payload?(payload)
|
|
639
|
+
|
|
640
|
+
start_new_streaming_turn.call
|
|
641
|
+
appended = append_wrapped_delta_text(current_turn_parts, payload)
|
|
642
|
+
current_turn_finalized_output = false if appended
|
|
643
|
+
saw_assistant_output ||= appended
|
|
644
|
+
when "agent_message"
|
|
645
|
+
next unless wrapped_assistant_payload?(payload)
|
|
646
|
+
|
|
647
|
+
wrapped_same_turn_finalization =
|
|
648
|
+
pending_turn_usage_source == :wrapped &&
|
|
649
|
+
pending_turn_usage &&
|
|
650
|
+
(
|
|
651
|
+
!current_turn_finalized_output ||
|
|
652
|
+
pending_wrapped_same_turn_finalization
|
|
653
|
+
)
|
|
654
|
+
start_new_turn.call
|
|
655
|
+
replace_current_turn_parts.call(extract_message_content_parts(payload))
|
|
656
|
+
pending_wrapped_same_turn_finalization = wrapped_same_turn_finalization
|
|
657
|
+
when "task_complete", "turn_complete"
|
|
658
|
+
completion_parts = extract_task_complete_parts(payload)
|
|
659
|
+
next if completion_parts.nil?
|
|
660
|
+
|
|
661
|
+
wrapped_same_turn_finalization =
|
|
662
|
+
pending_turn_usage_source == :wrapped &&
|
|
663
|
+
pending_turn_usage &&
|
|
664
|
+
(
|
|
665
|
+
!current_turn_finalized_output ||
|
|
666
|
+
pending_wrapped_same_turn_finalization
|
|
667
|
+
)
|
|
668
|
+
start_new_turn.call
|
|
669
|
+
replace_current_turn_parts.call(completion_parts)
|
|
670
|
+
pending_wrapped_same_turn_finalization = wrapped_same_turn_finalization
|
|
671
|
+
when "token_count"
|
|
672
|
+
wrapped_token_usage = extract_wrapped_tokens(payload["info"])
|
|
673
|
+
if wrapped_token_usage
|
|
674
|
+
has_usage = true
|
|
675
|
+
if wrapped_token_usage_starts_new_turn?(pending_turn_usage, pending_turn_usage_source, turn_completed, wrapped_token_usage)
|
|
676
|
+
commit_pending_turn.call
|
|
677
|
+
turn_completed = false
|
|
678
|
+
end
|
|
679
|
+
pending_turn_usage, pending_turn_usage_source = merge_wrapped_turn_usage(
|
|
680
|
+
pending_turn_usage,
|
|
681
|
+
pending_turn_usage_source,
|
|
682
|
+
wrapped_token_usage
|
|
683
|
+
)
|
|
684
|
+
pending_wrapped_output_parts =
|
|
685
|
+
(pending_turn_usage_source == :wrapped) ? current_turn_parts : nil
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
when "response_item"
|
|
689
|
+
payload = event["payload"]
|
|
690
|
+
next unless payload.is_a?(Hash) && response_item_assistant_payload?(payload)
|
|
691
|
+
|
|
692
|
+
start_new_finalized_turn.call
|
|
693
|
+
replace_current_turn_parts.call(extract_message_content_parts(payload))
|
|
694
|
+
pending_wrapped_same_turn_finalization =
|
|
695
|
+
pending_turn_usage_source == :wrapped && pending_turn_usage
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
raw_output.each_line do |line|
|
|
700
|
+
line = line.strip
|
|
701
|
+
next if line.empty?
|
|
702
|
+
|
|
703
|
+
begin
|
|
704
|
+
event = JSON.parse(line)
|
|
705
|
+
rescue JSON::ParserError
|
|
706
|
+
next
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
process_event.call(event)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
commit_pending_turn.call
|
|
713
|
+
final_parts = current_turn_parts.empty? ? latest_completed_parts : current_turn_parts
|
|
714
|
+
text = if final_parts.empty?
|
|
715
|
+
(turn_completed && saw_assistant_output) ? "" : nil
|
|
716
|
+
else
|
|
717
|
+
final_parts.join
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
{
|
|
721
|
+
text: text,
|
|
722
|
+
tokens: has_usage ? {
|
|
723
|
+
input: total_input,
|
|
724
|
+
output: total_output,
|
|
725
|
+
total: total_tokens
|
|
726
|
+
} : nil
|
|
727
|
+
}
|
|
728
|
+
rescue
|
|
729
|
+
nil
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def append_delta_text(parts, delta)
|
|
733
|
+
return false unless delta.is_a?(Hash)
|
|
734
|
+
|
|
735
|
+
delta_parts = extract_delta_content_parts(delta)
|
|
736
|
+
return false if delta_parts.nil?
|
|
737
|
+
|
|
738
|
+
appended = false
|
|
739
|
+
delta_parts.each do |part|
|
|
740
|
+
next if part.empty?
|
|
741
|
+
|
|
742
|
+
parts << part
|
|
743
|
+
appended = true
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
appended
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def append_wrapped_delta_text(parts, payload)
|
|
750
|
+
delta_parts = extract_wrapped_delta_parts(payload)
|
|
751
|
+
return false if delta_parts.nil?
|
|
752
|
+
|
|
753
|
+
appended = false
|
|
754
|
+
delta_parts.each do |part|
|
|
755
|
+
next if part.empty?
|
|
756
|
+
|
|
757
|
+
parts << part
|
|
758
|
+
appended = true
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
appended
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def assistant_message_item?(item)
|
|
765
|
+
item_role = item["role"]
|
|
766
|
+
item_type = item["type"]
|
|
767
|
+
item_item_type = item["item_type"]
|
|
768
|
+
message_shaped_item =
|
|
769
|
+
(
|
|
770
|
+
message_item_type?(item_type) ||
|
|
771
|
+
item_type == "agent_message"
|
|
772
|
+
) && assistant_message_item_type?(item_item_type)
|
|
773
|
+
|
|
774
|
+
(
|
|
775
|
+
item_role == "assistant" && message_shaped_item
|
|
776
|
+
) || (
|
|
777
|
+
item_role.nil? && message_shaped_item && (
|
|
778
|
+
item_type == "agent_message" ||
|
|
779
|
+
item_item_type == "assistant_message"
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def wrapped_assistant_payload?(payload)
|
|
785
|
+
role = payload["role"]
|
|
786
|
+
item_type = payload["item_type"]
|
|
787
|
+
|
|
788
|
+
assistant_message_item_type?(item_type) &&
|
|
789
|
+
(role == "assistant" || role.nil?)
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def response_item_assistant_payload?(payload)
|
|
793
|
+
payload_type = payload["type"]
|
|
794
|
+
payload_role = payload["role"]
|
|
795
|
+
payload_item_type = payload["item_type"]
|
|
796
|
+
assistant_message_type = payload_type == "assistant_message"
|
|
797
|
+
|
|
798
|
+
return false unless assistant_message_item_type?(payload_item_type)
|
|
799
|
+
|
|
800
|
+
((message_item_type?(payload_type) || payload_type == "agent_message" || assistant_message_type) && payload_role == "assistant") ||
|
|
801
|
+
(payload_type == "agent_message" && (
|
|
802
|
+
payload_role == "assistant" ||
|
|
803
|
+
(payload_role.nil? && assistant_message_item_type?(payload_item_type))
|
|
804
|
+
)) ||
|
|
805
|
+
(
|
|
806
|
+
assistant_message_type && (
|
|
807
|
+
payload_role == "assistant" ||
|
|
808
|
+
(payload_role.nil? && assistant_message_item_type?(payload_item_type))
|
|
809
|
+
)
|
|
810
|
+
) ||
|
|
811
|
+
(
|
|
812
|
+
payload_role.nil? &&
|
|
813
|
+
message_item_type?(payload_type) &&
|
|
814
|
+
payload_item_type == "assistant_message"
|
|
815
|
+
)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def assistant_message_item_type?(item_type)
|
|
819
|
+
item_type.nil? || item_type == "assistant_message"
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def message_item_type?(item_type)
|
|
823
|
+
item_type.nil? || item_type == "message"
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def extract_message_content_parts(item)
|
|
827
|
+
item_text = item["text"]
|
|
828
|
+
return [item_text] if item_text.is_a?(String) && !item_text.empty?
|
|
829
|
+
|
|
830
|
+
item_message = item["message"]
|
|
831
|
+
return [item_message] if item_message.is_a?(String) && !item_message.empty?
|
|
832
|
+
|
|
833
|
+
if item_text.is_a?(String)
|
|
834
|
+
return extract_fallback_content_parts(item, item_text)
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
if item_message.is_a?(String)
|
|
838
|
+
return extract_fallback_content_parts(item, item_message)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
item_content = item["content"]
|
|
842
|
+
return nil unless item_content.is_a?(Array)
|
|
843
|
+
|
|
844
|
+
extract_content_parts(item_content)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def extract_fallback_content_parts(item, empty_value)
|
|
848
|
+
item_content = item["content"]
|
|
849
|
+
return [empty_value] unless item_content.is_a?(Array)
|
|
850
|
+
|
|
851
|
+
content_parts = extract_content_parts(item_content)
|
|
852
|
+
content_parts.nil? ? [empty_value] : content_parts
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def extract_wrapped_delta_parts(payload)
|
|
856
|
+
delta = payload["delta"]
|
|
857
|
+
if delta.is_a?(Hash)
|
|
858
|
+
delta_parts = extract_delta_content_parts(delta)
|
|
859
|
+
return delta_parts unless delta_parts.nil?
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
extract_delta_content_parts(payload)
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def extract_task_complete_parts(payload)
|
|
866
|
+
last_agent_message = payload["last_agent_message"]
|
|
867
|
+
return [last_agent_message] if last_agent_message.is_a?(String)
|
|
868
|
+
return nil unless last_agent_message.is_a?(Hash)
|
|
869
|
+
return nil unless completed_assistant_message_payload?(last_agent_message)
|
|
870
|
+
|
|
871
|
+
extract_message_content_parts(last_agent_message)
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def completed_assistant_message_payload?(payload)
|
|
875
|
+
payload_role = payload["role"]
|
|
876
|
+
payload_type = payload["type"]
|
|
877
|
+
payload_item_type = payload["item_type"]
|
|
878
|
+
message_shaped_payload =
|
|
879
|
+
(
|
|
880
|
+
message_item_type?(payload_type) ||
|
|
881
|
+
payload_type == "agent_message" ||
|
|
882
|
+
payload_type == "assistant_message"
|
|
883
|
+
) && assistant_message_item_type?(payload_item_type)
|
|
884
|
+
|
|
885
|
+
(
|
|
886
|
+
payload_role == "assistant" && message_shaped_payload
|
|
887
|
+
) || (
|
|
888
|
+
payload_role.nil? && message_shaped_payload && (
|
|
889
|
+
payload_type.nil? ||
|
|
890
|
+
payload_type == "agent_message" ||
|
|
891
|
+
payload_type == "assistant_message" ||
|
|
892
|
+
payload_item_type == "assistant_message"
|
|
893
|
+
)
|
|
894
|
+
)
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def extract_delta_content_parts(item)
|
|
898
|
+
direct_parts = extract_message_content_parts(item)
|
|
899
|
+
return direct_parts unless direct_parts == [""]
|
|
900
|
+
|
|
901
|
+
item_content = item["content"]
|
|
902
|
+
return direct_parts unless item_content.is_a?(Array)
|
|
903
|
+
|
|
904
|
+
content_parts = extract_content_parts(item_content)
|
|
905
|
+
content_parts.nil? ? direct_parts : content_parts
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def output_text_block?(block)
|
|
909
|
+
block_type = block["type"]
|
|
910
|
+
|
|
911
|
+
block_type.nil? || block_type == "output_text" || block_type == "output_text_delta"
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def extract_content_parts(item_content)
|
|
915
|
+
completed_parts = []
|
|
916
|
+
extracted_content = false
|
|
917
|
+
|
|
918
|
+
item_content.each do |block|
|
|
919
|
+
next unless block.is_a?(Hash)
|
|
920
|
+
next unless output_text_block?(block)
|
|
921
|
+
|
|
922
|
+
block_text = block["text"]
|
|
923
|
+
next unless block_text.is_a?(String)
|
|
924
|
+
|
|
925
|
+
extracted_content = true
|
|
926
|
+
completed_parts << block_text
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
extracted_content ? completed_parts : nil
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def extract_wrapped_tokens(info)
|
|
933
|
+
return unless info.is_a?(Hash)
|
|
934
|
+
|
|
935
|
+
last_usage = build_token_usage(info["last_token_usage"])
|
|
936
|
+
total_usage = build_token_usage(info["total_token_usage"])
|
|
937
|
+
|
|
938
|
+
return unless last_usage || total_usage
|
|
939
|
+
|
|
940
|
+
{last: last_usage, total: total_usage}
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def token_usage_fields_present?(usage)
|
|
944
|
+
usage.is_a?(Hash) && (
|
|
945
|
+
!parse_token_count(usage["input_tokens"]).nil? ||
|
|
946
|
+
!parse_token_count(usage["cached_input_tokens"]).nil? ||
|
|
947
|
+
!parse_token_count(usage["output_tokens"]).nil? ||
|
|
948
|
+
!parse_token_count(usage["total_tokens"]).nil?
|
|
949
|
+
)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def build_token_usage(usage)
|
|
953
|
+
return unless token_usage_fields_present?(usage)
|
|
954
|
+
|
|
955
|
+
input_value = parse_token_count(usage["input_tokens"])
|
|
956
|
+
cached_input_value = parse_token_count(usage["cached_input_tokens"])
|
|
957
|
+
output_value = parse_token_count(usage["output_tokens"])
|
|
958
|
+
total_value = parse_token_count(usage["total_tokens"])
|
|
959
|
+
|
|
960
|
+
input = (input_value || 0) + (cached_input_value || 0)
|
|
961
|
+
output = output_value || 0
|
|
962
|
+
total = total_value || (input + output)
|
|
963
|
+
|
|
964
|
+
{
|
|
965
|
+
input: input,
|
|
966
|
+
output: output,
|
|
967
|
+
total: total,
|
|
968
|
+
input_reported: !input_value.nil? || !cached_input_value.nil?,
|
|
969
|
+
output_reported: !output_value.nil?,
|
|
970
|
+
total_reported: !total_value.nil?
|
|
971
|
+
}
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def merge_wrapped_turn_usage(existing_usage, existing_source, wrapped_token_usage)
|
|
975
|
+
total_usage = wrapped_token_usage[:total]
|
|
976
|
+
last_usage = wrapped_token_usage[:last]
|
|
977
|
+
|
|
978
|
+
if existing_source == :turn_completed
|
|
979
|
+
replacement_usage = merged_wrapped_usage(existing_usage, existing_source, last_usage, total_usage)
|
|
980
|
+
return [existing_usage, existing_source] unless replacement_usage
|
|
981
|
+
|
|
982
|
+
return [merge_same_turn_usage(existing_usage, replacement_usage), :turn_completed]
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
merged_usage = merged_wrapped_usage(existing_usage, existing_source, last_usage, total_usage)
|
|
986
|
+
[merged_usage, :wrapped]
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def merged_wrapped_usage(existing_usage, existing_source, last_usage, total_usage)
|
|
990
|
+
if last_usage
|
|
991
|
+
replacement_usage = last_usage
|
|
992
|
+
if total_usage && same_turn_usage?(replacement_usage, total_usage)
|
|
993
|
+
replacement_usage = merge_same_turn_usage(replacement_usage, total_usage)
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
return replacement_usage unless existing_source == :wrapped && existing_usage
|
|
997
|
+
|
|
998
|
+
merged_usage = add_token_usage(existing_usage, last_usage)
|
|
999
|
+
if total_usage && same_turn_usage?(merged_usage, total_usage)
|
|
1000
|
+
return merge_same_turn_usage(merged_usage, total_usage)
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
return replacement_usage if total_usage && same_turn_usage?(last_usage, total_usage)
|
|
1004
|
+
|
|
1005
|
+
return merged_usage
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
total_usage
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def wrapped_token_usage_starts_new_turn?(existing_usage, existing_source, turn_completed, wrapped_token_usage)
|
|
1012
|
+
return false unless turn_completed && existing_source == :turn_completed && existing_usage
|
|
1013
|
+
|
|
1014
|
+
candidate_usage = wrapped_token_usage[:total] || wrapped_token_usage[:last]
|
|
1015
|
+
return false unless candidate_usage
|
|
1016
|
+
|
|
1017
|
+
return false if same_turn_usage?(existing_usage, candidate_usage)
|
|
1018
|
+
|
|
1019
|
+
existing_detailed = existing_usage[:input_reported] && existing_usage[:output_reported]
|
|
1020
|
+
candidate_detailed = candidate_usage[:input_reported] && candidate_usage[:output_reported]
|
|
1021
|
+
existing_total_only = existing_usage[:total_reported] && !existing_detailed
|
|
1022
|
+
candidate_total_only = candidate_usage[:total_reported] && !candidate_detailed
|
|
1023
|
+
|
|
1024
|
+
return true if existing_detailed && candidate_detailed
|
|
1025
|
+
return true if existing_total_only && candidate_detailed
|
|
1026
|
+
|
|
1027
|
+
existing_total_only && candidate_total_only
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
def add_token_usage(left, right)
|
|
1031
|
+
{
|
|
1032
|
+
input: left[:input] + right[:input],
|
|
1033
|
+
output: left[:output] + right[:output],
|
|
1034
|
+
total: left[:total] + right[:total],
|
|
1035
|
+
input_reported: left[:input_reported] || right[:input_reported],
|
|
1036
|
+
output_reported: left[:output_reported] || right[:output_reported],
|
|
1037
|
+
total_reported: left[:total_reported] || right[:total_reported]
|
|
1038
|
+
}
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def merge_same_turn_usage(left, right)
|
|
1042
|
+
return right unless left
|
|
1043
|
+
return left unless right
|
|
1044
|
+
|
|
1045
|
+
merged_input_reported = left[:input_reported] || right[:input_reported]
|
|
1046
|
+
merged_output_reported = left[:output_reported] || right[:output_reported]
|
|
1047
|
+
merged_total_reported = left[:total_reported] || right[:total_reported]
|
|
1048
|
+
|
|
1049
|
+
input = if right[:input_reported]
|
|
1050
|
+
right[:input]
|
|
1051
|
+
elsif left[:input_reported]
|
|
1052
|
+
left[:input]
|
|
1053
|
+
else
|
|
1054
|
+
0
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
output = if right[:output_reported]
|
|
1058
|
+
right[:output]
|
|
1059
|
+
elsif left[:output_reported]
|
|
1060
|
+
left[:output]
|
|
1061
|
+
else
|
|
1062
|
+
0
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
total = if right[:total_reported]
|
|
1066
|
+
right[:total]
|
|
1067
|
+
elsif left[:total_reported]
|
|
1068
|
+
left[:total]
|
|
1069
|
+
else
|
|
1070
|
+
input + output
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
{
|
|
1074
|
+
input: input,
|
|
1075
|
+
output: output,
|
|
1076
|
+
total: total,
|
|
1077
|
+
input_reported: merged_input_reported,
|
|
1078
|
+
output_reported: merged_output_reported,
|
|
1079
|
+
total_reported: merged_total_reported
|
|
1080
|
+
}
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
def same_turn_usage?(left, right)
|
|
1084
|
+
return false unless left && right
|
|
1085
|
+
|
|
1086
|
+
detailed_usage_matches = left[:input_reported] &&
|
|
1087
|
+
right[:input_reported] &&
|
|
1088
|
+
left[:output_reported] &&
|
|
1089
|
+
right[:output_reported]
|
|
1090
|
+
return left[:input] == right[:input] && left[:output] == right[:output] if detailed_usage_matches
|
|
1091
|
+
|
|
1092
|
+
mixed_total_match = (
|
|
1093
|
+
left[:input_reported] &&
|
|
1094
|
+
left[:output_reported] &&
|
|
1095
|
+
right[:total_reported]
|
|
1096
|
+
) || (
|
|
1097
|
+
right[:input_reported] &&
|
|
1098
|
+
right[:output_reported] &&
|
|
1099
|
+
left[:total_reported]
|
|
1100
|
+
)
|
|
1101
|
+
return left[:total] == right[:total] if mixed_total_match
|
|
1102
|
+
|
|
1103
|
+
left[:total_reported] && right[:total_reported] && left[:total] == right[:total]
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def same_turn_output?(current_turn_parts, current_turn_finalized_output, result)
|
|
1107
|
+
return true if current_turn_parts.empty?
|
|
1108
|
+
return false unless current_turn_finalized_output
|
|
1109
|
+
return true unless result.is_a?(String)
|
|
1110
|
+
|
|
1111
|
+
current_turn_parts.join == result
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
def parse_token_count(value)
|
|
1115
|
+
case value
|
|
1116
|
+
when Integer
|
|
1117
|
+
value if value >= 0
|
|
1118
|
+
when String
|
|
1119
|
+
stripped = value.strip
|
|
1120
|
+
return nil unless /\A\d+\z/.match?(stripped)
|
|
1121
|
+
|
|
1122
|
+
stripped.to_i
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
|
|
357
1126
|
def externally_sandboxed?(options)
|
|
358
1127
|
if options.key?(:externally_sandboxed)
|
|
359
1128
|
!!options[:externally_sandboxed]
|