active_harness 0.2.30 → 0.2.31

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: f5f759449bd21052f986bc092a621f2ce39cb4faa630ba1bc759419bc27aac37
4
- data.tar.gz: 94e8abc9d0a0ca7ef877ea0f1e66ac68899236d7d33f10ac754f3541b3a29d1b
3
+ metadata.gz: d6d0a8c421353c670b4f0ba80cab409a05c97a1505382fd35a0822b39d66691a
4
+ data.tar.gz: a312b8db19958a621d26aee139d9bbe284f269690991b02008a2e6716fb08d7b
5
5
  SHA512:
6
- metadata.gz: 8a9529f9391337660a325993adf7db17bd2f470f6b2e69cab256a1ac907616d0c76a5c5b4c56f9850399fbfb8d886b3f2d2c2241c6ceb66ea0d83cb0e2d4c724
7
- data.tar.gz: e1ae97b30a763b761ae28d20b957670d5b8a774d249456cfd84da09830ce6ebc8661c2d19790a69ff2889ff585ce7af307beb13b5d46886708db041fd1c33d73
6
+ metadata.gz: bc0be024611a21164721a379741f48cf2547ebb4b5bd64f15bb3cdf5bae41e8bdbbe7f0d576c41ddd24a5167488bd2db2729cb659d699f3aceffda2791664998
7
+ data.tar.gz: 329d8648ae84605f6ecbd2ee9112a9a0abf39a4d3f4a2fb98ee6814b79e7bd8167a1fd807e1a994dc5d299475b02b603e48d65a4270fa01b9082ca5ecf499900
@@ -2,25 +2,23 @@ module ActiveHarness
2
2
  class Agent
3
3
  private
4
4
 
5
- # Calculates the monetary cost of a single request based on token usage
6
- # and pricing data from ActiveHarness::Costs.
5
+ # Builds a CostBreakdown for a single request from token usage and
6
+ # pricing data from ActiveHarness::Costs.
7
7
  #
8
- # Returns a hash { input_cost:, output_cost:, total_cost: } in USD,
8
+ # Returns CostBreakdown (input, output, total in USD),
9
9
  # or nil if usage is absent or the model is not found in the pricing registry.
10
- def calculate_cost(model_id, usage)
11
- return nil if model_id.nil? || usage.nil?
10
+ def calculate_cost(pricing, tokens)
11
+ return nil unless pricing && tokens
12
+ return nil unless pricing.input_per_million && pricing.output_per_million
12
13
 
13
- pricing = ActiveHarness::Costs.find(model_id.to_s)
14
- return nil unless pricing&.input_per_million && pricing&.output_per_million
14
+ input_cost = (tokens.input.to_f / 1_000_000) * pricing.input_per_million
15
+ output_cost = (tokens.output.to_f / 1_000_000) * pricing.output_per_million
15
16
 
16
- input_cost = (usage[:input_tokens].to_f / 1_000_000) * pricing.input_per_million
17
- output_cost = (usage[:output_tokens].to_f / 1_000_000) * pricing.output_per_million
18
-
19
- {
20
- input_cost: input_cost.round(8),
21
- output_cost: output_cost.round(8),
22
- total_cost: (input_cost + output_cost).round(8)
23
- }
17
+ CostBreakdown.new(
18
+ input: input_cost.round(8),
19
+ output: output_cost.round(8),
20
+ total: (input_cost + output_cost).round(8)
21
+ )
24
22
  rescue StandardError
25
23
  nil
26
24
  end
@@ -57,7 +57,13 @@ module ActiveHarness
57
57
  obj.instance_variable_set(:@params, @params)
58
58
  obj.instance_variable_set(:@config, @config)
59
59
  obj.instance_variable_set(:@memory, @memory)
60
- obj.instance_variable_set(:@context_window, @context_window)
60
+ obj.instance_variable_set(:@context_window, context_window_for_prompt)
61
+ end
62
+
63
+ def context_window_for_prompt
64
+ Costs.find(model_list.to_a.first&.dig(:model).to_s)&.context_window
65
+ rescue StandardError
66
+ nil
61
67
  end
62
68
  end
63
69
  end
@@ -54,7 +54,6 @@ module ActiveHarness
54
54
  :params,
55
55
  :memory
56
56
  attr_reader :result,
57
- :context_window,
58
57
  :token_stream,
59
58
  :event_stream
60
59
 
@@ -77,9 +76,8 @@ module ActiveHarness
77
76
  @context = context
78
77
  @params = params
79
78
  @memory = memory
80
- @models_override = Array(models) if models
81
- @context_window = lookup_context_window(self.models.to_a.first)
82
- @token_stream = streams[:token]
79
+ @models_override = Array(models) if models
80
+ @token_stream = streams[:token]
83
81
  @event_stream = streams[:agent]
84
82
  fire(:setup)
85
83
  end
@@ -146,31 +144,59 @@ module ActiveHarness
146
144
  end
147
145
 
148
146
  def build_result(response, entry, attempts, elapsed)
149
- raw = response[:content]
150
- processed = parse_output(raw)
151
- usage = response[:usage]
152
- cw = lookup_context_window(entry)
147
+ raw = response[:content]
148
+ processed = parse_output(raw)
149
+ raw_usage = response[:usage]
150
+ model_cost = lookup_model_cost(entry)
153
151
 
154
152
  Result.new(
155
153
  input: @input,
156
154
  output: raw,
157
155
  processed: processed,
158
156
  system_prompt: @system_prompt,
159
- provider: entry[:provider],
160
- model: entry[:model],
161
- temperature: entry[:temperature],
157
+ model: build_model_info(entry, model_cost),
162
158
  model_list: model_list,
163
159
  attempts: attempts,
164
160
  execution_time: elapsed,
165
- usage: usage,
166
- cost: calculate_cost(entry[:model], usage),
167
- context_window: cw
161
+ usage: build_usage(raw_usage, model_cost)
162
+ )
163
+ end
164
+
165
+ def build_model_info(entry, model_cost)
166
+ pricing = if model_cost&.input_per_million && model_cost&.output_per_million
167
+ ModelPricing.new(
168
+ input: (model_cost.input_per_million / 1_000_000.0).round(10),
169
+ output: (model_cost.output_per_million / 1_000_000.0).round(10)
170
+ )
171
+ end
172
+
173
+ ModelInfo.new(
174
+ name: entry[:model],
175
+ provider: entry[:provider],
176
+ temperature: entry[:temperature],
177
+ context_window: model_cost&.context_window,
178
+ pricing: pricing
179
+ )
180
+ end
181
+
182
+ def build_usage(raw_usage, model_cost)
183
+ return nil if raw_usage.nil?
184
+
185
+ tokens = TokenCounts.new(
186
+ input: raw_usage[:input_tokens],
187
+ output: raw_usage[:output_tokens],
188
+ total: raw_usage[:total_tokens]
189
+ )
190
+
191
+ UsageInfo.new(
192
+ tokens: tokens,
193
+ cost: calculate_cost(model_cost, tokens)
168
194
  )
169
195
  end
170
196
 
171
- def lookup_context_window(entry)
197
+ def lookup_model_cost(entry)
172
198
  return nil unless entry
173
- Costs.find(entry[:model])&.context_window
199
+ Costs.find(entry[:model].to_s)
174
200
  rescue StandardError
175
201
  nil
176
202
  end
@@ -1,24 +1,64 @@
1
1
  require "json"
2
2
 
3
3
  module ActiveHarness
4
- # Minimal result wrapper returned by Agent#call.
4
+ # Value objects for structured result data.
5
+
6
+ # Pricing rates for a model (per-token, from Costs registry).
7
+ # nil when the model is not found in the pricing registry.
8
+ ModelPricing = Struct.new(:input, :output, keyword_init: true)
9
+
10
+ # Static model metadata resolved at call time.
11
+ ModelInfo = Struct.new(
12
+ :name,
13
+ :provider,
14
+ :temperature,
15
+ :context_window,
16
+ :pricing, # ModelPricing or nil
17
+ keyword_init: true
18
+ ) do
19
+ def to_s; name.to_s; end
20
+ def inspect
21
+ parts = ["name=#{name.inspect}", "provider=#{provider.inspect}"]
22
+ parts << "temperature=#{temperature}" if temperature
23
+ parts << "context_window=#{context_window}" if context_window
24
+ parts << "pricing=#{pricing.inspect}" if pricing
25
+ "#<ModelInfo #{parts.join(' ')}>"
26
+ end
27
+ end
28
+
29
+ # Token counts for a single call.
30
+ TokenCounts = Struct.new(:input, :output, :total, keyword_init: true)
31
+
32
+ # Monetary cost of a single call in USD.
33
+ # nil when pricing data is unavailable.
34
+ CostBreakdown = Struct.new(:input, :output, :total, keyword_init: true)
35
+
36
+ # Combined token + cost stats for a single agent call.
37
+ # tokens is always present (raw provider data).
38
+ # cost is nil when pricing is unavailable.
39
+ UsageInfo = Struct.new(:tokens, :cost, keyword_init: true)
40
+
41
+ # Result returned by Agent#call (accessible via agent.result).
5
42
  #
6
- # output raw string from the provider
7
- # processed for format :json: a Ruby Hash/Array; for format :text: same as output
8
- # usage token counts: { input_tokens:, output_tokens:, total_tokens: } or nil for streaming
9
- # cost{ input_cost:, output_cost:, total_cost: } in USD, or nil if pricing unavailable
43
+ # result.input — original input string
44
+ # result.output raw string from the provider
45
+ # result.processed parsed Hash/Array for :json agents, raw string for :text
46
+ # result.system_prompt resolved system prompt string
47
+ # result.model — ModelInfo (name, provider, temperature, context_window, pricing)
48
+ # result.model_list — full model chain proxy
49
+ # result.attempts — Array of failed attempt entries before success
50
+ # result.execution_time — wall-clock seconds for the successful call
51
+ # result.usage — UsageInfo (tokens + cost), nil for streaming without usage
10
52
  Result = Struct.new(
11
53
  :input,
12
54
  :output,
13
55
  :processed,
14
56
  :system_prompt,
15
- :provider, :model,
16
- :temperature,
57
+ :model, # ModelInfo
17
58
  :model_list,
18
59
  :attempts,
19
60
  :execution_time,
20
- :usage,
21
- :cost,
22
- :context_window,
23
- keyword_init: true)
61
+ :usage, # UsageInfo or nil
62
+ keyword_init: true
63
+ )
24
64
  end
@@ -30,7 +30,7 @@ require_relative "active_harness/pipeline"
30
30
  require_relative "active_harness/railtie" if defined?(Rails::Railtie)
31
31
 
32
32
  module ActiveHarness
33
- VERSION = "0.2.30"
33
+ VERSION = "0.2.31"
34
34
 
35
35
  class << self
36
36
  # Configure ActiveHarness.
@@ -12,7 +12,7 @@ class AiSupportController < ApplicationController
12
12
 
13
13
  render json: {
14
14
  output: result.output,
15
- model: result.model,
15
+ model: result.model.name,
16
16
  time: result.execution_time
17
17
  }
18
18
  end
@@ -30,7 +30,7 @@ class AiSupportController < ApplicationController
30
30
 
31
31
  render json: {
32
32
  output: result.output,
33
- model: result.model,
33
+ model: result.model.name,
34
34
  time: result.execution_time,
35
35
  turns: memory.size
36
36
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.30
4
+ version: 0.2.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-11 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby