active_harness 0.2.37 → 0.2.39
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/lib/active_harness/agent/cost.rb +29 -5
- data/lib/active_harness/agent.rb +0 -12
- data/lib/active_harness/pipeline/step.rb +13 -9
- data/lib/active_harness/pipeline.rb +81 -12
- data/lib/active_harness/tribunal.rb +7 -1
- data/lib/active_harness.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 656a10b468592b8e0c53c2c93a8fbf7b8536420f94bd0cb7a61255d432ee78ed
|
|
4
|
+
data.tar.gz: 29de2c821b23ba363ef1596fb545035fbd1191ca6487161f6b9f30b39fe960db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d0d4c4114ed028a603a783841c5205163f1f02203e7c010d5c806a8e673644b391cbfb636bbb1c4d77eb40a807907e63380213b087ab3c5515491c1c2c64fcf
|
|
7
|
+
data.tar.gz: 84b154cb18717e6183337d728a6eb3ec9d34753856d6acc8fbf96c90e06a6e0db799b961b3f8d5bd7dc3984797bed401429a3110573243379eb471fcd84f160b
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
module ActiveHarness
|
|
2
2
|
class Agent
|
|
3
|
+
# Providers that have a dedicated pricing source beyond ModelsDev.
|
|
4
|
+
# Consulted in tier-2 before the general ModelsDev fallback.
|
|
5
|
+
# Add entries here when a new provider-specific source is available.
|
|
6
|
+
PROVIDER_PRICING_SOURCES = {
|
|
7
|
+
openrouter: Pricing::OpenRouter
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
3
10
|
private
|
|
4
11
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
12
|
+
# Cost lookup — three-tier fallback:
|
|
13
|
+
# 1. provider_cost in the API response (handled in build_usage)
|
|
14
|
+
# 2. provider-specific source (e.g. Pricing::OpenRouter for :openrouter)
|
|
15
|
+
# 3. Pricing::ModelsDev general fallback
|
|
16
|
+
# → nil when no data found at any tier
|
|
17
|
+
def lookup_model_cost(entry)
|
|
18
|
+
return nil unless entry
|
|
19
|
+
|
|
20
|
+
model = entry[:model].to_s
|
|
21
|
+
provider = entry[:provider].to_sym
|
|
22
|
+
|
|
23
|
+
source = PROVIDER_PRICING_SOURCES[provider]
|
|
24
|
+
cost = source&.find(model)
|
|
25
|
+
return cost if cost
|
|
26
|
+
|
|
27
|
+
Pricing::ModelsDev.find(model)
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Builds a CostBreakdown from token counts and per-million rates.
|
|
33
|
+
# Returns nil when pricing or token data is absent.
|
|
10
34
|
def calculate_cost(pricing, tokens)
|
|
11
35
|
return nil unless pricing && tokens
|
|
12
36
|
return nil unless pricing.input_per_million && pricing.output_per_million
|
data/lib/active_harness/agent.rb
CHANGED
|
@@ -198,18 +198,6 @@ module ActiveHarness
|
|
|
198
198
|
UsageInfo.new(tokens: tokens, cost: cost)
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
def lookup_model_cost(entry)
|
|
202
|
-
return nil unless entry
|
|
203
|
-
|
|
204
|
-
if entry[:provider].to_sym == :openrouter
|
|
205
|
-
Pricing::OpenRouter.find(entry[:model].to_s) || Pricing.find(entry[:model].to_s)
|
|
206
|
-
else
|
|
207
|
-
Pricing.find(entry[:model].to_s)
|
|
208
|
-
end
|
|
209
|
-
rescue StandardError
|
|
210
|
-
nil
|
|
211
|
-
end
|
|
212
|
-
|
|
213
201
|
def normalize_input!
|
|
214
202
|
return if @config.fetch(:normalize_input, true) == false
|
|
215
203
|
@input = @input&.strip&.gsub(/\s+/, " ")
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
module ActiveHarness
|
|
2
2
|
class Pipeline
|
|
3
3
|
class Step
|
|
4
|
-
attr_reader :name, :
|
|
4
|
+
attr_reader :name, :executor
|
|
5
5
|
|
|
6
|
-
def initialize(name,
|
|
7
|
-
@name
|
|
8
|
-
@
|
|
9
|
-
@stop_if
|
|
6
|
+
def initialize(name, executor = nil, &block)
|
|
7
|
+
@name = name
|
|
8
|
+
@executor = executor
|
|
9
|
+
@stop_if = nil
|
|
10
10
|
instance_eval(&block) if block_given?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# DSL: use TranslationAgent
|
|
13
|
+
# DSL: use TranslationAgent / SafetyTribunal / NestedPipeline
|
|
14
14
|
def use(klass)
|
|
15
|
-
@
|
|
15
|
+
@executor = klass
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# DSL (inside block): stop_if ->(result) { ... }
|
|
@@ -35,9 +35,13 @@ module ActiveHarness
|
|
|
35
35
|
@transform_block
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
def lambda?
|
|
39
|
+
@executor.is_a?(Proc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True if the executor is a Tribunal subclass — tribunal steps do not update payload.
|
|
39
43
|
def tribunal?
|
|
40
|
-
@
|
|
44
|
+
@executor.is_a?(Class) && @executor <= ActiveHarness::Tribunal
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
# Returns true when this step should update the pipeline payload after execution.
|
|
@@ -29,7 +29,7 @@ module ActiveHarness
|
|
|
29
29
|
# pipeline.call
|
|
30
30
|
# pipeline.output # => final payload string (nil if stopped)
|
|
31
31
|
# pipeline.stopped? # => false
|
|
32
|
-
# pipeline.
|
|
32
|
+
# pipeline.steps { |name, result| ... } # iterate over completed steps
|
|
33
33
|
#
|
|
34
34
|
class Pipeline
|
|
35
35
|
# -------------------------------------------------------------------------
|
|
@@ -46,8 +46,8 @@ module ActiveHarness
|
|
|
46
46
|
# use InjectionGuardAgent
|
|
47
47
|
# stop_if ->(result) { result.processed["detected"] == true }
|
|
48
48
|
# end
|
|
49
|
-
def step(name,
|
|
50
|
-
pipeline_config[:steps] << Pipeline::Step.new(name,
|
|
49
|
+
def step(name, executor = nil, &block)
|
|
50
|
+
pipeline_config[:steps] << Pipeline::Step.new(name, executor, &block)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def pipeline_config
|
|
@@ -100,7 +100,6 @@ module ActiveHarness
|
|
|
100
100
|
:stopped_at,
|
|
101
101
|
:stop_reason,
|
|
102
102
|
:execution_time,
|
|
103
|
-
:step_results,
|
|
104
103
|
:context
|
|
105
104
|
attr_writer :context
|
|
106
105
|
attr_accessor :params
|
|
@@ -126,6 +125,7 @@ module ActiveHarness
|
|
|
126
125
|
@token = token
|
|
127
126
|
class_streams = self.class.pipeline_config[:streams] || {}
|
|
128
127
|
@stream = merge_stream(stream, class_streams)
|
|
128
|
+
@executors = build_executors
|
|
129
129
|
@step_results = {}
|
|
130
130
|
@stopped = false
|
|
131
131
|
@stopped_at = nil
|
|
@@ -134,10 +134,44 @@ module ActiveHarness
|
|
|
134
134
|
@output = nil
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
# Returns a hash of pre-created executor instances keyed by step name.
|
|
138
|
+
# Available immediately after new — before call — so instances can be
|
|
139
|
+
# configured (model lists, params, etc.) before execution begins.
|
|
140
|
+
#
|
|
141
|
+
# pipeline = SupportPipeline.new(input: "...")
|
|
142
|
+
# pipeline.executors[:translate].models.prepend(provider: :openai, model: "gpt-4.1")
|
|
143
|
+
# pipeline.executors[:guard].params = { strict: true }
|
|
144
|
+
# pipeline.call
|
|
145
|
+
def executors
|
|
146
|
+
@executors
|
|
147
|
+
end
|
|
148
|
+
|
|
137
149
|
def stopped?
|
|
138
150
|
@stopped
|
|
139
151
|
end
|
|
140
152
|
|
|
153
|
+
# Iterates over completed steps as (name, executor, result) tuples.
|
|
154
|
+
# Steps that did not run (pipeline stopped before reaching them) are skipped.
|
|
155
|
+
# Returns an Enumerator when called without a block.
|
|
156
|
+
#
|
|
157
|
+
# pipeline.steps { |name, executor, result| }
|
|
158
|
+
#
|
|
159
|
+
# name — step name symbol (:translate, :guard, …)
|
|
160
|
+
# executor — the instance that ran (TranslationAgent instance, …)
|
|
161
|
+
# result — Result struct (output, processed, usage, model, …)
|
|
162
|
+
#
|
|
163
|
+
# pipeline.steps.map { |name, executor, result| [name, result.output] }
|
|
164
|
+
# pipeline.steps.to_a
|
|
165
|
+
def steps
|
|
166
|
+
return enum_for(:steps) unless block_given?
|
|
167
|
+
self.class.pipeline_config[:steps].each do |step|
|
|
168
|
+
result = @step_results[step.name]
|
|
169
|
+
next unless result
|
|
170
|
+
yield step.name, @executors[step.name], result
|
|
171
|
+
end
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
141
175
|
# Wraps pipeline outcome into a Result so a pipeline can be used as a step
|
|
142
176
|
# inside another pipeline, matching the same interface as Agent and Tribunal.
|
|
143
177
|
#
|
|
@@ -153,7 +187,15 @@ module ActiveHarness
|
|
|
153
187
|
end
|
|
154
188
|
|
|
155
189
|
# Execute all steps sequentially. Returns self for chaining.
|
|
156
|
-
|
|
190
|
+
# Accepts optional input, token, stream to match the Agent/Tribunal call interface.
|
|
191
|
+
def call(input = nil, token: nil, stream: nil)
|
|
192
|
+
if input
|
|
193
|
+
@original_input = input
|
|
194
|
+
@payload = input
|
|
195
|
+
end
|
|
196
|
+
@token = token if token
|
|
197
|
+
@stream = stream if stream
|
|
198
|
+
|
|
157
199
|
config = self.class.pipeline_config
|
|
158
200
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
201
|
|
|
@@ -231,13 +273,40 @@ module ActiveHarness
|
|
|
231
273
|
end
|
|
232
274
|
|
|
233
275
|
def execute_step(step)
|
|
234
|
-
step.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
276
|
+
if step.lambda?
|
|
277
|
+
execute_lambda_step(step)
|
|
278
|
+
else
|
|
279
|
+
instance = @executors[step.name]
|
|
280
|
+
instance.context = @context.dup
|
|
281
|
+
instance.call(@payload).result
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def execute_lambda_step(step)
|
|
286
|
+
lam = step.executor
|
|
287
|
+
has_kw = lam.parameters.any? { |type, _| [:key, :keyreq, :keyrest].include?(type) }
|
|
288
|
+
result = has_kw ? lam.call(@payload, context: @context.dup, params: @params) : lam.call(@payload)
|
|
289
|
+
|
|
290
|
+
unless result.is_a?(Result)
|
|
291
|
+
raise ArgumentError,
|
|
292
|
+
"Lambda step :#{step.name} must return ActiveHarness::Result, got #{result.class}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
result
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def build_executors
|
|
299
|
+
self.class.pipeline_config[:steps].each_with_object({}) do |step, hash|
|
|
300
|
+
next if step.lambda?
|
|
301
|
+
|
|
302
|
+
hash[step.name] = step.executor.new(
|
|
303
|
+
input: @payload,
|
|
304
|
+
context: @context.dup,
|
|
305
|
+
params: @params,
|
|
306
|
+
token: @token,
|
|
307
|
+
stream: @stream
|
|
308
|
+
)
|
|
309
|
+
end
|
|
241
310
|
end
|
|
242
311
|
end
|
|
243
312
|
end
|
|
@@ -105,11 +105,17 @@ module ActiveHarness
|
|
|
105
105
|
# Run all agents in parallel, then compute the verdict.
|
|
106
106
|
# Returns self so calls can be chained: tribunal.call.verdict
|
|
107
107
|
#
|
|
108
|
+
# Accepts an optional input to update payload before running — matches
|
|
109
|
+
# the Agent#call(input) interface so tribunals work as pipeline executors.
|
|
110
|
+
#
|
|
108
111
|
# Behaviour on failure:
|
|
109
112
|
# - If some agents fail/timeout, their errors are in #errors and
|
|
110
113
|
# #results contains only successful results.
|
|
111
114
|
# - If ALL agents fail/timeout, raises Errors::AllAgentsFailed.
|
|
112
|
-
def call
|
|
115
|
+
def call(input = nil, token: nil, stream: nil)
|
|
116
|
+
@input = input if input
|
|
117
|
+
@token = token if token
|
|
118
|
+
@stream = stream if stream
|
|
113
119
|
agents = resolve_agents
|
|
114
120
|
fire(:before_call)
|
|
115
121
|
|
data/lib/active_harness.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.39
|
|
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
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|