agent-harness 0.7.0 → 0.7.2
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 +16 -0
- data/lib/agent_harness/providers/adapter.rb +14 -0
- data/lib/agent_harness/providers/anthropic.rb +45 -0
- data/lib/agent_harness/providers/base.rb +11 -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: c8c1873c6be023d4ab91659139b56b89552b8a0e89c101fed27ef0113c868434
|
|
4
|
+
data.tar.gz: 5bd2792791e5e7f1d8ee7cdd49b93bb9ee7c3d7e762914d2e3d4b4d8bfb014d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8530d91fec6ebddae4d0c8cb101a75c18df480ee15ae5006957576c20596ac199f0546a72e8d128dbcf8223eb39c7f7af6e7abe7aba05e67e27bda68c6b0bd5
|
|
7
|
+
data.tar.gz: 4ee7d860aa222170d8e3edd9319fd31eae0d174e571a8da0fae540b1fb5f6094c329ca0431a879f6d4927df7396e6a28aa2dedbae4467fa3d4cd8ed744829f34
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.2](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.1...agent-harness/v0.7.2) (2026-04-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 113: [P1] feat: support disabling tools for text-only send_message calls ([#115](https://github.com/viamin/agent-harness/issues/115)) ([62bc66a](https://github.com/viamin/agent-harness/commit/62bc66a3d34a889de65ba7c4951b8bdb1f388fa9))
|
|
9
|
+
|
|
10
|
+
## [0.7.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.7.0...agent-harness/v0.7.1) (2026-04-15)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **codex:** address remaining json output review feedback ([505e068](https://github.com/viamin/agent-harness/commit/505e068d63fbc5590112ba00faee0d1c62d997e3))
|
|
16
|
+
* **codex:** address review feedback for token usage extraction ([398940e](https://github.com/viamin/agent-harness/commit/398940ecb356ec9e6978d42244ae26295823bb89))
|
|
17
|
+
* **codex:** preserve output-last-message flag values ([7480778](https://github.com/viamin/agent-harness/commit/7480778d6a5d7b9227eec20889bc642eb399d1b5))
|
|
18
|
+
|
|
3
19
|
## [0.7.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.6.0...agent-harness/v0.7.0) (2026-04-13)
|
|
4
20
|
|
|
5
21
|
|
|
@@ -717,6 +717,13 @@ module AgentHarness
|
|
|
717
717
|
# @option options [Integer] :timeout timeout in seconds
|
|
718
718
|
# @option options [String] :session session identifier
|
|
719
719
|
# @option options [Boolean] :dangerous_mode skip permission checks
|
|
720
|
+
# @option options [Symbol, Array<String>, nil] :tools tool access control.
|
|
721
|
+
# Pass +:none+ to disable all tool access (pure text-in/text-out mode).
|
|
722
|
+
# Pass an Array of tool name strings to selectively disable specific
|
|
723
|
+
# tools via the provider's disallowed-tools mechanism. Defaults to +nil+
|
|
724
|
+
# (tools enabled, provider default behavior).
|
|
725
|
+
# Providers that do not support tool control will emit a warning and
|
|
726
|
+
# ignore this option — it is never a hard failure.
|
|
720
727
|
# @option options [ProviderRuntime, Hash, nil] :provider_runtime per-request
|
|
721
728
|
# runtime overrides (model, base_url, api_provider, env, flags, metadata).
|
|
722
729
|
# For providers that delegate to Providers::Base#send_message, a plain Hash
|
|
@@ -839,6 +846,13 @@ module AgentHarness
|
|
|
839
846
|
end
|
|
840
847
|
end
|
|
841
848
|
|
|
849
|
+
# Check if provider supports tool access control (disabling tools)
|
|
850
|
+
#
|
|
851
|
+
# @return [Boolean] true if the provider supports the tools: option
|
|
852
|
+
def supports_tool_control?
|
|
853
|
+
false
|
|
854
|
+
end
|
|
855
|
+
|
|
842
856
|
# Check if provider supports dangerous mode
|
|
843
857
|
#
|
|
844
858
|
# @return [Boolean] true if dangerous mode is supported
|
|
@@ -317,6 +317,10 @@ module AgentHarness
|
|
|
317
317
|
["--mcp-config", config_path]
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
+
def supports_tool_control?
|
|
321
|
+
true
|
|
322
|
+
end
|
|
323
|
+
|
|
320
324
|
def dangerous_mode_flags
|
|
321
325
|
["--dangerously-skip-permissions"]
|
|
322
326
|
end
|
|
@@ -401,6 +405,22 @@ module AgentHarness
|
|
|
401
405
|
|
|
402
406
|
protected
|
|
403
407
|
|
|
408
|
+
# All tools the Claude CLI exposes by default.
|
|
409
|
+
# Used to build the --disallowedTools list when tools: :none is requested.
|
|
410
|
+
ALL_CLI_TOOLS = %w[
|
|
411
|
+
Agent
|
|
412
|
+
Bash
|
|
413
|
+
Read
|
|
414
|
+
Edit
|
|
415
|
+
Write
|
|
416
|
+
Grep
|
|
417
|
+
Glob
|
|
418
|
+
WebFetch
|
|
419
|
+
WebSearch
|
|
420
|
+
TodoWrite
|
|
421
|
+
NotebookEdit
|
|
422
|
+
].freeze
|
|
423
|
+
|
|
404
424
|
def build_command(prompt, options)
|
|
405
425
|
cmd = [self.class.binary_name]
|
|
406
426
|
|
|
@@ -411,6 +431,14 @@ module AgentHarness
|
|
|
411
431
|
cmd += ["--model", @config.model]
|
|
412
432
|
end
|
|
413
433
|
|
|
434
|
+
# Add permission mode for tool-disabled requests (belt-and-suspenders)
|
|
435
|
+
if options[:tools]
|
|
436
|
+
# Skip --permission-mode plan when dangerous_mode is active, since
|
|
437
|
+
# --dangerously-skip-permissions would override it anyway.
|
|
438
|
+
# The --disallowedTools flags still provide the primary protection.
|
|
439
|
+
cmd += build_tool_control_flags(options[:tools], skip_permission_mode: options[:dangerous_mode])
|
|
440
|
+
end
|
|
441
|
+
|
|
414
442
|
# Add dangerous mode if requested
|
|
415
443
|
if options[:dangerous_mode] && supports_dangerous_mode?
|
|
416
444
|
cmd += dangerous_mode_flags
|
|
@@ -612,6 +640,23 @@ module AgentHarness
|
|
|
612
640
|
end
|
|
613
641
|
end
|
|
614
642
|
|
|
643
|
+
def build_tool_control_flags(tools_option, skip_permission_mode: false)
|
|
644
|
+
tool_names = case tools_option
|
|
645
|
+
when :none
|
|
646
|
+
ALL_CLI_TOOLS
|
|
647
|
+
when Array
|
|
648
|
+
tools_option
|
|
649
|
+
else
|
|
650
|
+
return []
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
return [] if tool_names.empty?
|
|
654
|
+
|
|
655
|
+
flags = tool_names.flat_map { |tool| ["--disallowedTools", tool] }
|
|
656
|
+
flags = ["--permission-mode", "plan"] + flags unless skip_permission_mode
|
|
657
|
+
flags
|
|
658
|
+
end
|
|
659
|
+
|
|
615
660
|
def log_debug(action, **context)
|
|
616
661
|
@logger&.debug("[AgentHarness::Anthropic] #{action}: #{context.inspect}")
|
|
617
662
|
end
|
|
@@ -104,6 +104,17 @@ module AgentHarness
|
|
|
104
104
|
def send_message(prompt:, **options)
|
|
105
105
|
log_debug("send_message_start", prompt_length: prompt.length, options: options.keys)
|
|
106
106
|
|
|
107
|
+
# Warn when tools option is passed to a provider that doesn't support it
|
|
108
|
+
if options[:tools] && !supports_tool_control?
|
|
109
|
+
log_debug("tools_option_unsupported",
|
|
110
|
+
provider: self.class.provider_name,
|
|
111
|
+
tools: options[:tools])
|
|
112
|
+
@logger&.warn(
|
|
113
|
+
"[AgentHarness::#{self.class.provider_name}] tools option is not supported " \
|
|
114
|
+
"by this provider and will be ignored"
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
107
118
|
# Coerce provider_runtime from Hash if needed
|
|
108
119
|
options = normalize_provider_runtime(options)
|
|
109
120
|
|
|
@@ -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]
|