active_harness 0.2.14 → 0.2.16

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: 6a42e03d329e638e3119d0a259a2286f05a0ec1603b8c2c81636e4ac041ebf9d
4
- data.tar.gz: 074cf7e1558d9efee20cd17c3405665be00ef29c30574289b54d6e2251ebeacc
3
+ metadata.gz: 26824b777c1b0a68676671e07725a4ec171bb1491cf306b098aa97c6d4cf5510
4
+ data.tar.gz: bd0ca2f2fb3fa813eeea21bd48e49435babd989b761e5ea4a6dda194218a27ae
5
5
  SHA512:
6
- metadata.gz: 3397bd1901b3062030b3a41dc327ed1fd7053ef5d90e8e53d0ebb820375054f112cc80e70e528282703277e90137310318d026c456950f86ae755a09aca4e473
7
- data.tar.gz: 7137703c7469a5f1cd8adfb121fd5047231387890756325f7e035955c7a23dab3bd0dfaf22c223343a9fb006279b6b352c01f1cf443da6cfcf60c3e26227cac3
6
+ metadata.gz: b51494c59af3664360e3a0cdaa650a309499bada4df3a10f46bb1c4f547e577276f27bcdfa2c5ebbe2e88a220245feca1c51aba018ecee495cb15d72c5a076bb
7
+ data.tar.gz: e770331ab6e5621fc5f55257bd736d24eb972dbcd800075113971665c652e3d553505488ae8dec58d184c1f8cc5fc971146861d7e264cd4e291c4d18ccd694ae
@@ -23,6 +23,14 @@ module ActiveHarness
23
23
  def inherited(subclass)
24
24
  subclass.instance_variable_set(:@agent_config, {})
25
25
  end
26
+
27
+ # Automatically strip and collapse whitespace in @input before each call.
28
+ # Enabled by default. Disable with:
29
+ #
30
+ # normalize_input false
31
+ def normalize_input(value = true)
32
+ agent_config[:normalize_input] = value
33
+ end
26
34
  end
27
35
 
28
36
  # -------------------------------------------------------------------------
@@ -42,8 +50,9 @@ module ActiveHarness
42
50
 
43
51
  def initialize(input: nil, context: {}, models: nil, memory: nil, stream: nil, token_stream: nil, event_stream: nil)
44
52
  @input = input
45
- @context = context
46
53
  @config = self.class.agent_config
54
+ normalize_input!
55
+ @context = context
47
56
  @models_override = Array(models) if models
48
57
  @stream = stream
49
58
  @token_stream = token_stream
@@ -60,7 +69,10 @@ module ActiveHarness
60
69
  # agent.call("What is the capital of Japan?")
61
70
  # agent.call("...", stream: ->(token) { print token })
62
71
  def call(input = nil, token_stream: nil)
63
- @input = input if input
72
+ if input
73
+ @input = input
74
+ normalize_input!
75
+ end
64
76
  @token_stream = token_stream if token_stream
65
77
  @memory&.load
66
78
  @system_prompt = resolve_system_prompt
@@ -145,6 +157,11 @@ module ActiveHarness
145
157
  model: result.model
146
158
  )
147
159
  end
160
+
161
+ def normalize_input!
162
+ return if @config.fetch(:normalize_input, true) == false
163
+ @input = @input&.strip&.gsub(/\s+/, " ")
164
+ end
148
165
  end
149
166
  end
150
167
 
@@ -9,13 +9,46 @@ module ActiveHarness
9
9
  tribunal_config[:agents] = list.flatten
10
10
  end
11
11
 
12
- # Class-level process block — defines how to compute the verdict from results.
12
+ # Class-level process block — defines how to compute the verdict from all results.
13
+ # Receives the full results array; return value becomes #verdict.
14
+ # Takes priority over +verdict+ strategy if both are declared.
13
15
  #
14
16
  # process { |results| results.all? { |r| r.parsed["result"] == true } }
15
- # process { |results| results.count { |r| r.parsed["result"] == true } >= 2 }
16
17
  def process(&block)
17
18
  tribunal_config[:process] = block
18
19
  end
20
+
21
+ # Declarative verdict — built-in aggregation strategy with a per-result evaluator.
22
+ #
23
+ # Strategies:
24
+ # :unanimous — verdict true when every successful result evaluates to true
25
+ # :majority — verdict true when more than half of successful results evaluate to true
26
+ #
27
+ # Options:
28
+ # may_fail: N — tolerate up to N agent errors before raising AllAgentsFailed
29
+ # (default: nil — raise only when all agents fail, preserving legacy behavior)
30
+ #
31
+ # The block receives a single Result and must return a truthy/falsy value.
32
+ #
33
+ # verdict :unanimous do |result|
34
+ # result.parsed["result"] == true
35
+ # end
36
+ #
37
+ # verdict :majority, may_fail: 1 do |result|
38
+ # result.parsed["result"] == true
39
+ # end
40
+ VALID_STRATEGIES = %i[unanimous majority].freeze
41
+
42
+ def verdict(strategy, may_fail: nil, &block)
43
+ unless VALID_STRATEGIES.include?(strategy)
44
+ raise ArgumentError,
45
+ "Unknown verdict strategy :#{strategy}. Valid strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(", ")}"
46
+ end
47
+
48
+ tribunal_config[:strategy] = strategy
49
+ tribunal_config[:may_fail] = may_fail
50
+ tribunal_config[:evaluate_block] = block
51
+ end
19
52
  end
20
53
  end
21
54
  end
@@ -0,0 +1,44 @@
1
+ module ActiveHarness
2
+ class Tribunal
3
+ # Instance-level process block — overrides class-level block.
4
+ #
5
+ # tribunal.process { |results| results.count { |r| r.parsed["ok"] } >= 2 }
6
+ def process(&block)
7
+ @process_block = block
8
+ self
9
+ end
10
+
11
+ private
12
+
13
+ # Selects and executes the verdict computation in order of priority:
14
+ # 1. Instance-level process block (set via tribunal.process { ... })
15
+ # 2. Class-level process block (set via `process do ... end` in subclass)
16
+ # 3. Class-level strategy (set via `verdict :unanimous/:majority`)
17
+ # 4. nil (no verdict computation declared)
18
+ def compute_verdict(results)
19
+ if @process_block
20
+ @process_block.call(results)
21
+ elsif @strategy
22
+ apply_strategy(@strategy, results)
23
+ end
24
+ end
25
+
26
+ # Built-in aggregation strategies.
27
+ #
28
+ # Without an evaluate_block every successful result is treated as a positive vote.
29
+ # With an evaluate_block the block decides whether each result counts as positive.
30
+ #
31
+ # :unanimous — all positive votes required
32
+ # :majority — more than 50% positive votes required
33
+ def apply_strategy(strategy, results)
34
+ evaluate = @evaluate_block || ->(r) { r }
35
+ votes = results.map { |r| evaluate.call(r) ? true : false }
36
+ positive = votes.count(true)
37
+
38
+ case strategy
39
+ when :unanimous then positive == votes.size
40
+ when :majority then positive > votes.size / 2.0
41
+ end
42
+ end
43
+ end
44
+ end
@@ -50,7 +50,8 @@ module ActiveHarness
50
50
  attr_reader :results, :errors, :verdict, :execution_time, :agent_execution_times
51
51
 
52
52
  def initialize(input: nil, context: {}, agents: nil, timeout: 7,
53
- stream: nil, agent_event_stream: nil, tribunal_event_stream: nil)
53
+ stream: nil, agent_event_stream: nil, tribunal_event_stream: nil,
54
+ may_fail: :_unset)
54
55
  config = self.class.tribunal_config
55
56
 
56
57
  @input = input
@@ -58,6 +59,9 @@ module ActiveHarness
58
59
  @agents = agents || config[:agents]
59
60
  @timeout = timeout
60
61
  @process_block = config[:process]
62
+ @strategy = config[:strategy]
63
+ @evaluate_block = config[:evaluate_block]
64
+ @may_fail = may_fail == :_unset ? config[:may_fail] : may_fail
61
65
  @hooks = config[:hooks].dup
62
66
  @stream = stream
63
67
  @agent_event_stream = agent_event_stream
@@ -69,12 +73,6 @@ module ActiveHarness
69
73
  @agent_execution_times = []
70
74
  end
71
75
 
72
- # Instance-level process block — overrides class-level block.
73
- def process(&block)
74
- @process_block = block
75
- self
76
- end
77
-
78
76
  # Run all agents in parallel, then compute the verdict.
79
77
  # Returns self so calls can be chained: tribunal.call.verdict
80
78
  #
@@ -125,13 +123,13 @@ module ActiveHarness
125
123
 
126
124
  run_hook(:after_call, @results, @errors)
127
125
 
128
- if @results.empty?
129
- messages = @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
130
- raise Errors::AllAgentsFailed, "All agents failed — #{messages}"
131
- end
126
+ # If all agents failed, raise an exception.
127
+ # Otherwise, compute the verdict based on successful results.
128
+ check_failure_threshold!
132
129
 
133
130
  verdict_input = transform_hook(:before_verdict, @results)
134
- @verdict = @process_block ? @process_block.call(verdict_input) : nil
131
+ @verdict = compute_verdict(verdict_input)
132
+
135
133
  run_hook(:after_verdict, @verdict)
136
134
 
137
135
  self
@@ -139,6 +137,19 @@ module ActiveHarness
139
137
 
140
138
  private
141
139
 
140
+ def check_failure_threshold!
141
+ if !@may_fail.nil? && @errors.size > @may_fail
142
+ raise Errors::AllAgentsFailed,
143
+ "Too many agents failed (#{@errors.size} > may_fail: #{@may_fail}) — #{error_summary}"
144
+ elsif @results.empty?
145
+ raise Errors::AllAgentsFailed, "All agents failed — #{error_summary}"
146
+ end
147
+ end
148
+
149
+ def error_summary
150
+ @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
151
+ end
152
+
142
153
  def resolve_agents
143
154
  @agents.map do |agent|
144
155
  if agent.is_a?(Class)
@@ -161,3 +172,4 @@ end
161
172
 
162
173
  require_relative "tribunal/hooks"
163
174
  require_relative "tribunal/dsl"
175
+ require_relative "tribunal/processing"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.14
4
+ version: 0.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher
@@ -74,6 +74,7 @@ files:
74
74
  - lib/active_harness/tribunal.rb
75
75
  - lib/active_harness/tribunal/dsl.rb
76
76
  - lib/active_harness/tribunal/hooks.rb
77
+ - lib/active_harness/tribunal/processing.rb
77
78
  - lib/generators/active_harness/agent/agent_generator.rb
78
79
  - lib/generators/active_harness/agent/templates/agent.rb.tt
79
80
  - lib/generators/active_harness/install/install_generator.rb