active_harness 0.2.27 → 0.2.29

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: 5b406bc5feeebdb4cc26e215a65d25dd931605684c6f05ee7cdd71de5910a1d6
4
- data.tar.gz: a34d2e22840ae906d47b0e27fe271d97b42d97a0fce64a19a58e15c296f00bf7
3
+ metadata.gz: 7146a492cd9454703ab47eb1c6222962587b4fe5585aaef2a5390349da394a1a
4
+ data.tar.gz: 476ee4d22259b10fa7d58cd25208ea5c900e6523991b1f17e288f7c3104cfd92
5
5
  SHA512:
6
- metadata.gz: f53ee25213ac7404840349cee732aa59b1dc25168b4e362028e7542fd51a0c51e9f2a2af4b8cbad9a552e74a9c4769d147b7d642e3b051df9c293af58bc87f34
7
- data.tar.gz: d56efdd52b38afd9153903611fd860e316a59eb8f5d5b5c3ed0510b22647b8008a3b22f4e0c8430322548bb82a50c69cc4d6c086e8d965653c22d795fd43cae7
6
+ metadata.gz: d211c987a80244262c35fbc83202491b61807ee98f092808ca801cfcc57eb1ad58e4f48ccf8e364078b99a267e14816e8551b55c5b2a3de2fe8e6828321bc2b6
7
+ data.tar.gz: 71244082f0e0c81d9fe7d0271e05dd53ebf0e5a727ced5554d637e84aed8a58b6a014e8b0537ff2dcc10cc501385905b78c5aae8f328c1d774a874fc30d3c7b3
@@ -49,11 +49,15 @@ module ActiveHarness
49
49
  prompt
50
50
  end
51
51
 
52
+ # Injects agent state into a prompt class instance before #call.
53
+ # Available in prompt classes: @input, @context, @params, @memory, @context_window, @config
52
54
  def inject_agent_state(obj)
53
- obj.instance_variable_set(:@input, @input)
54
- obj.instance_variable_set(:@context, @context)
55
- obj.instance_variable_set(:@config, @config)
56
- obj.instance_variable_set(:@memory, @memory)
55
+ obj.instance_variable_set(:@input, @input)
56
+ obj.instance_variable_set(:@context, @context)
57
+ obj.instance_variable_set(:@params, @params)
58
+ obj.instance_variable_set(:@config, @config)
59
+ obj.instance_variable_set(:@memory, @memory)
60
+ obj.instance_variable_set(:@context_window, @context_window)
57
61
  end
58
62
  end
59
63
  end
@@ -54,6 +54,7 @@ module ActiveHarness
54
54
  :params,
55
55
  :memory
56
56
  attr_reader :result,
57
+ :context_window,
57
58
  :token_stream,
58
59
  :event_stream
59
60
 
@@ -76,9 +77,10 @@ module ActiveHarness
76
77
  @context = context
77
78
  @params = params
78
79
  @memory = memory
79
- @models_override = Array(models) if models
80
- @token_stream = streams[:token]
81
- @event_stream = streams[:agent]
80
+ @models_override = Array(models) if models
81
+ @context_window = lookup_context_window(self.models.to_a.first)
82
+ @token_stream = streams[:token]
83
+ @event_stream = streams[:agent]
82
84
  fire(:setup)
83
85
  end
84
86
 
@@ -144,9 +146,10 @@ module ActiveHarness
144
146
  end
145
147
 
146
148
  def build_result(response, entry, attempts, elapsed)
147
- raw = response[:content]
149
+ raw = response[:content]
148
150
  processed = parse_output(raw)
149
- usage = response[:usage]
151
+ usage = response[:usage]
152
+ cw = lookup_context_window(entry)
150
153
 
151
154
  Result.new(
152
155
  input: @input,
@@ -160,10 +163,18 @@ module ActiveHarness
160
163
  attempts: attempts,
161
164
  execution_time: elapsed,
162
165
  usage: usage,
163
- cost: calculate_cost(entry[:model], usage)
166
+ cost: calculate_cost(entry[:model], usage),
167
+ context_window: cw
164
168
  )
165
169
  end
166
170
 
171
+ def lookup_context_window(entry)
172
+ return nil unless entry
173
+ Costs.find(entry[:model])&.context_window
174
+ rescue StandardError
175
+ nil
176
+ end
177
+
167
178
  def normalize_input!
168
179
  return if @config.fetch(:normalize_input, true) == false
169
180
  @input = @input&.strip&.gsub(/\s+/, " ")
@@ -62,12 +62,33 @@ module ActiveHarness
62
62
  :output_per_million,
63
63
  :cache_read_input_per_million,
64
64
  :cache_write_input_per_million,
65
+ :context_window,
66
+ :max_output_tokens,
67
+ :input_modalities,
68
+ :output_modalities,
65
69
  keyword_init: true
66
70
  ) do
71
+ # Returns capability tags derived from modality data and model id/name.
72
+ # Possible values: "vision", "pdf", "audio", "video", "imggen", "embed"
73
+ def categories
74
+ inp = input_modalities || []
75
+ out = output_modalities || []
76
+ cats = []
77
+ cats << "vision" if inp.include?("image")
78
+ cats << "pdf" if inp.include?("pdf")
79
+ cats << "audio" if inp.include?("audio") || out.include?("audio")
80
+ cats << "video" if inp.include?("video") || out.include?("video")
81
+ cats << "imggen" if out.include?("image")
82
+ cats << "embed" if id.to_s.match?(/embed/i) || name.to_s.match?(/embed/i)
83
+ cats
84
+ end
85
+
67
86
  def inspect
68
87
  parts = ["id=#{id.inspect}", "provider=#{provider.inspect}"]
69
88
  parts << "input=$#{input_per_million}/M" if input_per_million
70
89
  parts << "output=$#{output_per_million}/M" if output_per_million
90
+ parts << "ctx=#{context_window}" if context_window
91
+ parts << "cats=#{categories.join(',')}" if categories.any?
71
92
  "#<ModelCost #{parts.join(' ')}>"
72
93
  end
73
94
  end
@@ -229,11 +250,16 @@ module ActiveHarness
229
250
  cache_write_input_per_million: cost[:cache_write]
230
251
  }.compact
231
252
 
253
+ mods = m[:modalities] || {}
232
254
  {
233
- id: m[:id],
234
- name: m[:name] || m[:id],
235
- provider: ah_provider,
236
- pricing: standard.any? ? { text_tokens: { standard: standard } } : {}
255
+ id: m[:id],
256
+ name: m[:name] || m[:id],
257
+ provider: ah_provider,
258
+ context_window: m[:context_window] || m.dig(:limit, :context),
259
+ max_output_tokens: m[:max_output_tokens] || m.dig(:limit, :output),
260
+ input_modalities: Array(mods[:input]),
261
+ output_modalities: Array(mods[:output]),
262
+ pricing: standard.any? ? { text_tokens: { standard: standard } } : {}
237
263
  }
238
264
  end
239
265
  end
@@ -248,7 +274,11 @@ module ActiveHarness
248
274
  input_per_million: standard[:input_per_million],
249
275
  output_per_million: standard[:output_per_million],
250
276
  cache_read_input_per_million: standard[:cache_read_input_per_million],
251
- cache_write_input_per_million: standard[:cache_write_input_per_million]
277
+ cache_write_input_per_million: standard[:cache_write_input_per_million],
278
+ context_window: raw[:context_window],
279
+ max_output_tokens: raw[:max_output_tokens],
280
+ input_modalities: Array(raw[:input_modalities]),
281
+ output_modalities: Array(raw[:output_modalities])
252
282
  )
253
283
  end
254
284
 
@@ -84,6 +84,12 @@ module ActiveHarness
84
84
  async: false,
85
85
  **adapter_opts
86
86
  )
87
+ if self.class == Memory
88
+ raise NotImplementedError,
89
+ "Memory cannot be instantiated directly. " \
90
+ "Use Memory::JsonFile, Memory::Postgresql, or Memory::Sqlite."
91
+ end
92
+
87
93
  @session_id = session_id
88
94
  @depth = depth
89
95
  @enabled = enabled
@@ -138,13 +144,15 @@ module ActiveHarness
138
144
 
139
145
  # Returns messages array for LLM consumption, respecting depth.
140
146
  # Optional filters:
141
- # filter: ->(turn) { turn[:agent] == "SupportAgent" }
142
- # since: Time.now - 3600
143
- def to_messages(filter: nil, since: nil)
147
+ # filter: ->(turn) { turn[:agent] == "SupportAgent" }
148
+ # since: Time.now - 3600
149
+ # token_budget: 4000 # rough limit (chars / 4 estimate); trims oldest turns first
150
+ def to_messages(filter: nil, since: nil, token_budget: nil)
144
151
  turns = @turns.dup
145
152
  turns.select! { |t| filter.call(t) } if filter
146
153
  turns.select! { |t| after?(t, since) } if since
147
154
  turns = turns.last(@depth) if @depth
155
+ turns = trim_to_token_budget(turns, token_budget) if token_budget
148
156
 
149
157
  turns.flat_map do |t|
150
158
  [
@@ -210,5 +218,16 @@ module ActiveHarness
210
218
  turn_time = Time.parse(turn[:at].to_s) rescue nil
211
219
  turn_time ? turn_time >= time : true
212
220
  end
221
+
222
+ def trim_to_token_budget(turns, budget)
223
+ return turns if turns.empty?
224
+
225
+ total = 0
226
+ turns.reverse.take_while do |t|
227
+ tokens = ((t[:request].to_s.length + t[:response].to_s.length) / 4.0).ceil
228
+ total += tokens
229
+ total <= budget
230
+ end.reverse
231
+ end
213
232
  end
214
233
  end
@@ -16,7 +16,7 @@ class SupportPipeline < ActiveHarness::Pipeline
16
16
 
17
17
  step :safety_tribunal do
18
18
  use SafetyTribunal
19
- stop_if ->(result) { result.verdict == false }
19
+ stop_if ->(result) { result.processed["verdict"] == false }
20
20
  end
21
21
  end
22
22
 
@@ -14,7 +14,7 @@ module ActiveHarness
14
14
  #
15
15
  # step :safety_tribunal do
16
16
  # use SafetyTribunal
17
- # stop_if ->(result) { result.verdict == false }
17
+ # stop_if ->(result) { result.processed["verdict"] == false }
18
18
  # end
19
19
  #
20
20
  # on :before_step do |step_name, payload| ... end
@@ -11,7 +11,7 @@ module ActiveHarness
11
11
  :input,
12
12
  :output,
13
13
  :processed,
14
- :system_prompt,
14
+ :system_prompt,
15
15
  :provider, :model,
16
16
  :temperature,
17
17
  :model_list,
@@ -19,5 +19,6 @@ module ActiveHarness
19
19
  :execution_time,
20
20
  :usage,
21
21
  :cost,
22
+ :context_window,
22
23
  keyword_init: true)
23
24
  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.27"
33
+ VERSION = "0.2.29"
34
34
 
35
35
  class << self
36
36
  # Configure ActiveHarness.
@@ -43,11 +43,12 @@ class AiSupportController < ApplicationController
43
43
  # Returns verdict: true (safe) or false (rejected).
44
44
  # ---------------------------------------------------------------------------
45
45
  def tribunal
46
- result = SupportGuardTribunal.call(input: params.require(:input))
46
+ tribunal = SupportGuardTribunal.new(input: params.require(:input))
47
+ tribunal.call
47
48
 
48
49
  render json: {
49
- verdict: result.verdict,
50
- time: result.execution_time
50
+ verdict: tribunal.verdict,
51
+ time: tribunal.execution_time
51
52
  }
52
53
  end
53
54
 
@@ -7,7 +7,7 @@ class SupportPipeline < ActiveHarness::Pipeline
7
7
  # Step 1 — GUARD: reject spam before spending tokens on an answer
8
8
  step :spam_guard do
9
9
  use SupportGuardTribunal
10
- stop_if ->(result) { result.verdict == false }
10
+ stop_if ->(result) { result.processed["verdict"] == false }
11
11
  end
12
12
 
13
13
  # Step 2 — RESPOND: generate the actual answer
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.27
4
+ version: 0.2.29
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-09 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby