bitfab 0.66.0 → 0.67.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: 35b3530a206cb86806f06bd4cbeef8d660b07f118f656dae1bc81d9bb5418038
4
- data.tar.gz: 3da107dd35bde42d9cd9f4c7f549226cf52624da941a24333c6b2ff7738dfd61
3
+ metadata.gz: 455ac9a2dfa11b39c17550b3f86f6ffa87f3ea0a9684fb032722184a705da7e0
4
+ data.tar.gz: 11c0f2491a21515f3fc75f4f4f8eb98f544b7e6d5d3c9d1c7fc09e0728787b16
5
5
  SHA512:
6
- metadata.gz: 191854786921184a3324c8fc7aa5bb620c82310e28129e33986cde9aa77ddbdb9fdf0da917b7829a07ba501f2c3992b8d6e0742a7ca695abaa1e3dffcac69826
7
- data.tar.gz: 9e642a61b8fb8287f6db422cf32d55a44565e9ff2caffa5716d0bf5800248780e620ba0f41ef1f10a35505079099f0f50fca519172185b559ba5c41256a99191
6
+ metadata.gz: d1f41b10072dd2b9b52f2a91ef5265a91805d2aa8129a05a30a303f6f55831934c8206eca6fd215ac938a526f499e9bc2b38a2e74476c8aa06e6c675e14d9b7f
7
+ data.tar.gz: d1e5d1ac8216e5505b5d5da628d6914349fe7ee6018fa55616b20a5ba3ca66e647484f4db176ee85feac28c476f702e831bfcf9ae3fa5ff5084d9d0570584ade
data/lib/bitfab/jev.rb ADDED
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ module Jev
5
+ PROVIDER = "typesafe"
6
+ SYSTEM_ONE = :system_one
7
+ WRAPPER_SCOPE_KEY = :bitfab_jev_wrapper_scope
8
+
9
+ module_function
10
+
11
+ def jev_shaped?(reply)
12
+ return false if reply.nil? || reply.is_a?(String) || reply.is_a?(Numeric) || reply.is_a?(Symbol)
13
+
14
+ usage = field(reply, :usage)
15
+ field(reply, :model).is_a?(String) && !usage.nil? && !usage.is_a?(String) && !usage.is_a?(Numeric)
16
+ rescue
17
+ false
18
+ end
19
+
20
+ def auto_context(reply, duration_ms)
21
+ jev_shaped?(reply) ? context(reply, duration_ms) : nil
22
+ rescue
23
+ nil
24
+ end
25
+
26
+ def mark_recorded(reply)
27
+ Thread.current[WRAPPER_SCOPE_KEY]&.push(reply)
28
+ end
29
+
30
+ def watch_recorded
31
+ previous = Thread.current[WRAPPER_SCOPE_KEY]
32
+ scope = []
33
+ Thread.current[WRAPPER_SCOPE_KEY] = scope
34
+ reply = yield
35
+ [reply, !scope.empty?]
36
+ ensure
37
+ Thread.current[WRAPPER_SCOPE_KEY] = previous
38
+ previous&.concat(scope) if scope
39
+ end
40
+
41
+ def record_node_call(method_name)
42
+ return yield unless method_name.to_sym == SYSTEM_ONE
43
+
44
+ span = Bitfab.current_span
45
+ started_at = monotonic_ms
46
+ reply, recorded_inside = watch_recorded { yield }
47
+ begin
48
+ entry = recorded_inside ? nil : auto_context(reply, monotonic_ms - started_at)
49
+ if entry
50
+ span.add_context(entry)
51
+ mark_recorded(reply)
52
+ Subtree.active_sessions.each(&:jev_recorded_below!)
53
+ end
54
+ rescue
55
+ nil
56
+ end
57
+ reply
58
+ end
59
+
60
+ def field(value, name)
61
+ if value.is_a?(Hash)
62
+ value.key?(name) ? value[name] : value[name.to_s]
63
+ elsif value.respond_to?(name)
64
+ value.public_send(name)
65
+ end
66
+ end
67
+
68
+ def context(reply, duration_ms)
69
+ return nil if reply.nil?
70
+
71
+ usage = field(reply, :usage)
72
+ model = field(reply, :model)
73
+ input_tokens = usage.nil? ? nil : field(usage, :input_tokens)
74
+ output_tokens = usage.nil? ? nil : field(usage, :output_tokens)
75
+ entry = {provider: PROVIDER}
76
+ entry[:model] = model if model.is_a?(String)
77
+ entry[:inputTokens] = input_tokens if input_tokens.is_a?(Numeric)
78
+ entry[:outputTokens] = output_tokens if output_tokens.is_a?(Numeric)
79
+ entry[:durationMs] = duration_ms
80
+ entry
81
+ end
82
+
83
+ def record(span, reply, duration_ms)
84
+ entry = context(reply, duration_ms)
85
+ span.add_context(entry) if entry
86
+ rescue
87
+ nil
88
+ end
89
+
90
+ def monotonic_ms
91
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
92
+ end
93
+
94
+ class Wrapper
95
+ def initialize(callable, on_reply)
96
+ @callable = callable
97
+ @on_reply = on_reply
98
+ end
99
+
100
+ # Call the wrapped Jev method with the given arguments and return its
101
+ # reply unchanged. Errors from the call are raised as-is and record nothing.
102
+ def call(*args, **kwargs, &block)
103
+ span = Bitfab.current_span
104
+ started_at = Jev.monotonic_ms
105
+ reply, recorded = Jev.watch_recorded { @callable.call(*args, **kwargs, &block) }
106
+ Jev.record(span, reply, Jev.monotonic_ms - started_at) unless recorded
107
+ begin
108
+ @on_reply&.call(reply)
109
+ rescue
110
+ nil
111
+ end
112
+ reply
113
+ end
114
+
115
+ # A Proc that calls this wrapper, so it can be passed with `&`.
116
+ def to_proc
117
+ method(:call).to_proc
118
+ end
119
+ end
120
+ end
121
+
122
+ class Client
123
+ # Record the model, token usage, and duration of a Jev (TypeSafe System One)
124
+ # call on the span that is current when the call starts. Opens no span of
125
+ # its own and never raises from recording.
126
+ #
127
+ # @example
128
+ # ask_jev = Bitfab.client.wrap_jev(jev.method(:system_one))
129
+ # reply = ask_jev.call(state:, questions:)
130
+ #
131
+ # @param callable [#call, nil] calls Jev and returns its reply; pass a block instead to wrap the block
132
+ # @param on_reply [#call, nil] called with each reply after it is recorded; its errors are ignored
133
+ # @return [Bitfab::Jev::Wrapper] responds to +call+ with the same arguments as the wrapped callable
134
+ def wrap_jev(callable = nil, on_reply: nil, &block)
135
+ raise ArgumentError, "wrap_jev takes a callable or a block, not both." if callable && block
136
+ target = callable || block
137
+ raise ArgumentError, "wrap_jev requires a callable that calls Jev (e.g., jev.method(:system_one))." unless target.respond_to?(:call)
138
+
139
+ Jev::Wrapper.new(target, on_reply)
140
+ end
141
+ end
142
+
143
+ class BitfabFunction
144
+ # Jev wrappers record onto the currently active span under any key.
145
+ def wrap_jev(*args, **kwargs, &block)
146
+ @client.wrap_jev(*args, **kwargs, &block)
147
+ end
148
+ end
149
+ end
@@ -37,6 +37,7 @@ module Bitfab
37
37
  :name,
38
38
  :excluded,
39
39
  :passthrough,
40
+ :jev,
40
41
  keyword_init: true
41
42
  )
42
43
 
@@ -55,6 +56,8 @@ module Bitfab
55
56
  :declared_node,
56
57
  :contexts,
57
58
  :prompt,
59
+ :jev_started_at,
60
+ :jev_recorded_below,
58
61
  keyword_init: true
59
62
  )
60
63
 
@@ -506,7 +509,7 @@ module Bitfab
506
509
 
507
510
  return if event.event == :call && skip_boundary_call?(event)
508
511
 
509
- resolved_path = owned_path(event.path)
512
+ resolved_path = traced_path(event)
510
513
  return unless resolved_path
511
514
 
512
515
  case event.event
@@ -523,6 +526,12 @@ module Bitfab
523
526
  @exclude[method_name.to_s] == true
524
527
  end
525
528
 
529
+ def jev_recorded_below!
530
+ @stacks[Fiber.current.object_id]&.each do |entry|
531
+ entry.jev_recorded_below = true if entry.is_a?(OpenSpan)
532
+ end
533
+ end
534
+
526
535
  def reserve_node
527
536
  stack = current_stack(Fiber.current.object_id)
528
537
  return true if captured_depth(stack) < @max_depth
@@ -684,7 +693,9 @@ module Bitfab
684
693
  args:,
685
694
  kwargs:,
686
695
  started_at: now,
687
- span_data: without_content ? {"content_off_by_simulation_plan" => true} : nil
696
+ span_data: without_content ? {"content_off_by_simulation_plan" => true} : nil,
697
+ span_type: metadata.jev ? "llm" : nil,
698
+ jev_started_at: metadata.jev ? Jev.monotonic_ms : nil
688
699
  )
689
700
  end
690
701
 
@@ -708,6 +719,7 @@ module Bitfab
708
719
  return unless entry.is_a?(OpenSpan) && matching_identity?(entry.identity, event, resolved_path)
709
720
 
710
721
  stack.pop
722
+ record_jev_reply(entry, event.return_value) if entry.jev_started_at
711
723
  emit(entry, result: event.return_value)
712
724
  parent = stack.last
713
725
  parent.error ||= entry.error if entry.error && parent.is_a?(OpenSpan)
@@ -715,6 +727,17 @@ module Bitfab
715
727
  @drained = true if @exhausted && captured_calls_drained?
716
728
  end
717
729
 
730
+ def record_jev_reply(entry, reply)
731
+ return if entry.error || entry.jev_recorded_below
732
+
733
+ context = Jev.auto_context(reply, Jev.monotonic_ms - entry.jev_started_at)
734
+ return unless context
735
+
736
+ entry.contexts = [context]
737
+ Jev.mark_recorded(reply)
738
+ jev_recorded_below!
739
+ end
740
+
718
741
  def record_error(event)
719
742
  entry = @stacks[Fiber.current.object_id]&.last
720
743
  return unless entry.is_a?(OpenSpan)
@@ -849,7 +872,8 @@ module Bitfab
849
872
  lineno: event.lineno,
850
873
  name: span_name(event, method_id),
851
874
  excluded: @exclude[method_id.to_s],
852
- passthrough: passthrough_wrapper?(event.parameters)
875
+ passthrough: passthrough_wrapper?(event.parameters),
876
+ jev: method_id == Jev::SYSTEM_ONE
853
877
  ).freeze
854
878
  end
855
879
 
@@ -862,6 +886,16 @@ module Bitfab
862
886
  event.callee_id || event.method_id
863
887
  end
864
888
 
889
+ def traced_path(event)
890
+ owned_path(event.path) || jev_path(event)
891
+ end
892
+
893
+ def jev_path(event)
894
+ return nil unless called_method_id(event) == Jev::SYSTEM_ONE && !sdk_frame?(event.path)
895
+
896
+ Subtree.realpath(event.path)
897
+ end
898
+
865
899
  def owned_path(path)
866
900
  @owned_paths.fetch(path) do
867
901
  resolved = Subtree.realpath(path)
@@ -945,7 +979,7 @@ module Bitfab
945
979
  return unless event.event == :call
946
980
  return if skip_boundary_call?(event)
947
981
 
948
- resolved_path = owned_path(event.path)
982
+ resolved_path = traced_path(event)
949
983
  count_dropped_call(event, resolved_path) if resolved_path
950
984
  rescue Exception # rubocop:disable Lint/RescueException
951
985
  nil
@@ -173,7 +173,9 @@ module Bitfab
173
173
  explicit_span_receiver: self, explicit_span_method_name: method_name,
174
174
  experiment_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?, declared_node: true,
175
175
  on_start: call.method(:start), on_complete: call.method(:complete),
176
- around_body: call.method(:run)) { super(*args, **kwargs, &block) }
176
+ around_body: call.method(:run)) do
177
+ Jev.record_node_call(method_name) { super(*args, **kwargs, &block) }
178
+ end
177
179
  end
178
180
  end
179
181
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.66.0"
4
+ VERSION = "0.67.0"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -30,6 +30,7 @@ require_relative "bitfab/replay_branch"
30
30
  require_relative "bitfab/client"
31
31
  require_relative "bitfab/seed"
32
32
  require_relative "bitfab/baml"
33
+ require_relative "bitfab/jev"
33
34
  require_relative "bitfab/traceable"
34
35
  require_relative "bitfab/replay_registry"
35
36
  require_relative "bitfab/seed_cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.66.0
4
+ version: 0.67.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -175,6 +175,7 @@ files:
175
175
  - lib/bitfab/git_state.rb
176
176
  - lib/bitfab/graders.rb
177
177
  - lib/bitfab/http_client.rb
178
+ - lib/bitfab/jev.rb
178
179
  - lib/bitfab/labels.rb
179
180
  - lib/bitfab/mock_override.rb
180
181
  - lib/bitfab/organization_members.rb