active_harness 0.2.13 → 0.2.15

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: 0c5339e68dc9f7de87fc67a4e986cd1858e86f52e95ee277ee0f12563eedf428
4
- data.tar.gz: d563d7088e6b29c2119160a9eb0e0a39eb550d12e1bf828937a7d20fddcf7ad4
3
+ metadata.gz: cc551806a5848e9157c21ee668ee38c139a55c46fb7e8c2a6a2e8022200c90cb
4
+ data.tar.gz: f6110846af88ce73b7fe7a30221d18960a58e80adeaf01e2b19b5e14fac0640d
5
5
  SHA512:
6
- metadata.gz: 9d710fedd64ae1bd1ee40616aacb884e698491ec223895651c1ad28a307121575f5078df54074e3218ade227cf376f20a881af15ecef640e63f74887cd5a9115
7
- data.tar.gz: '075778aea6ae0b53624b18fd64835cdaaec6e0381ff7616faab86c2b9813d5ae80830bdcf59ce71b255a13930ad6d35f99b1953bde11e58c7a8bda03c36732df'
6
+ metadata.gz: d8884d36a5da2581707584068acc7315a00d417bd88a7776d1702c375e05470257c21cedb6c323fec5aa75562e6f118d635ae1b1a2053d3e7d60b48b0aca8d3f
7
+ data.tar.gz: 02fa25dca8678816e76b2c2b2fad6131e26a7aa8b5a97cb78a5bcee84d7a9923507eb5981d0508663ecced5a6eb85c5f92799a00d80b59098252dc08438ff22f
@@ -0,0 +1,54 @@
1
+ module ActiveHarness
2
+ class Tribunal
3
+ class << self
4
+ # Declare agents at the class level.
5
+ #
6
+ # agents PolitenessAgent, ConstructivenessAgent
7
+ # agents [PolitenessAgent, ConstructivenessAgent]
8
+ def agents(*list)
9
+ tribunal_config[:agents] = list.flatten
10
+ end
11
+
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.
15
+ #
16
+ # process { |results| results.all? { |r| r.parsed["result"] == true } }
17
+ def process(&block)
18
+ tribunal_config[:process] = block
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
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ module ActiveHarness
2
+ class Tribunal
3
+ VALID_HOOKS = %i[
4
+ before_call
5
+ before_agent
6
+ after_agent
7
+ agent_error
8
+ after_call
9
+ before_verdict
10
+ after_verdict
11
+ ].freeze
12
+
13
+ class << self
14
+ # Class-level hook registration.
15
+ #
16
+ # on :before_call do ... end
17
+ # on :before_agent do |agent| ... end
18
+ # on :after_agent do |result| ... end
19
+ # on :agent_error do |name, error| ... end
20
+ # on :after_call do |results, errors| ... end
21
+ # on :before_verdict do |results| results end # transform hook
22
+ # on :after_verdict do |verdict| ... end
23
+ def on(event, &block)
24
+ unless VALID_HOOKS.include?(event)
25
+ raise ArgumentError,
26
+ "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
27
+ end
28
+
29
+ tribunal_config[:hooks][event] = block
30
+ end
31
+
32
+ # Rails-style aliases for +on+:
33
+ #
34
+ # before :call do ... end # → on :before_call
35
+ # before :agent do |agent| end # → on :before_agent
36
+ # before :verdict do |results| end # → on :before_verdict (transform)
37
+ # after :call do |r, e| end # → on :after_call
38
+ # after :agent do |result| end # → on :after_agent
39
+ # after :verdict do |verdict| end # → on :after_verdict
40
+ # callback :agent_error do |name, e| end # → on :agent_error
41
+ def before(event, &block)
42
+ on(:"before_#{event}", &block)
43
+ end
44
+
45
+ def after(event, &block)
46
+ on(:"after_#{event}", &block)
47
+ end
48
+
49
+ def callback(event, &block)
50
+ on(event, &block)
51
+ end
52
+ end
53
+
54
+ # Instance-level hook registration — overrides class-level hooks for this instance.
55
+ # :before_verdict is a transform hook: its return value replaces the results array
56
+ # passed to the process block.
57
+ def on(event, &block)
58
+ unless VALID_HOOKS.include?(event)
59
+ raise ArgumentError,
60
+ "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
61
+ end
62
+
63
+ @hooks[event] = block
64
+ self
65
+ end
66
+
67
+ private
68
+
69
+ def run_hook(event, *args)
70
+ return unless @hooks[event]
71
+
72
+ if args.any?
73
+ instance_exec(*args, &@hooks[event])
74
+ else
75
+ instance_eval(&@hooks[event])
76
+ end
77
+ end
78
+
79
+ # Like run_hook but uses the return value to replace the passed value.
80
+ # Used by :before_verdict to allow results transformation before verdict computation.
81
+ def transform_hook(event, value)
82
+ return value unless @hooks[event]
83
+
84
+ instance_exec(value, &@hooks[event])
85
+ end
86
+ end
87
+ 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
@@ -29,62 +29,11 @@ module ActiveHarness
29
29
  # ContentQualityTribunal.new(input: "...").call
30
30
  #
31
31
  class Tribunal
32
- VALID_HOOKS = %i[
33
- before_call
34
- after_agent
35
- agent_error
36
- after_call
37
- before_verdict
38
- after_verdict
39
- ].freeze
40
-
41
32
  # -------------------------------------------------------------------------
42
- # Class-level DSL — used when subclassing ActiveHarness::Tribunal
33
+ # Class-level DSL — core
43
34
  # -------------------------------------------------------------------------
44
35
  class << self
45
- # Declare agents at the class level.
46
- # agents PolitenessAgent, ConstructivenessAgent
47
- def agents(*list)
48
- tribunal_config[:agents] = list.flatten
49
- end
50
-
51
- # Class-level hook registration.
52
- # on(:after_agent) { |result| puts result.model }
53
- def on(event, &block)
54
- unless VALID_HOOKS.include?(event)
55
- raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
56
- end
57
-
58
- tribunal_config[:hooks][event] = block
59
- end
60
-
61
- # Rails-style aliases for +on+:
62
- #
63
- # before :call do ... end # → on :before_call
64
- # before :agent do ... end # → on :before_agent (not used yet)
65
- # before :verdict do |r| end # → on :before_verdict
66
- # after :call do ... end # → on :after_call
67
- # after :agent do |r| end # → on :after_agent
68
- # after :verdict do |v| end # → on :after_verdict
69
- # callback :agent_error do |n,e| end # → on :agent_error
70
- def before(event, &block)
71
- on(:"before_#{event}", &block)
72
- end
73
-
74
- def after(event, &block)
75
- on(:"after_#{event}", &block)
76
- end
77
-
78
- def callback(event, &block)
79
- on(event, &block)
80
- end
81
-
82
- # Class-level process block.
83
- # process { |results| results.all? { |r| r.parsed["result"] == true } }
84
- def process(&block)
85
- tribunal_config[:process] = block
86
- end
87
-
36
+ # Each subclass gets its own isolated config hash.
88
37
  def tribunal_config
89
38
  @tribunal_config ||= { agents: [], hooks: {} }
90
39
  end
@@ -94,42 +43,36 @@ module ActiveHarness
94
43
  end
95
44
  end
96
45
 
97
- attr_accessor :input, :context
46
+ # -------------------------------------------------------------------------
47
+ # Instance API
48
+ # -------------------------------------------------------------------------
49
+ attr_accessor :input, :context, :stream, :agent_event_stream, :tribunal_event_stream
98
50
  attr_reader :results, :errors, :verdict, :execution_time, :agent_execution_times
99
51
 
100
- def initialize(input: nil, context: {}, agents: nil, timeout: 7)
52
+ def initialize(input: nil, context: {}, agents: nil, timeout: 7,
53
+ stream: nil, agent_event_stream: nil, tribunal_event_stream: nil,
54
+ may_fail: :_unset)
101
55
  config = self.class.tribunal_config
102
56
 
103
- @input = input
104
- @context = context
105
- @agents = agents || config[:agents]
106
- @timeout = timeout
107
- @process_block = config[:process]
108
- @hooks = config[:hooks].dup
109
- @results = []
110
- @errors = []
111
- @verdict = nil
112
- @execution_time = nil
57
+ @input = input
58
+ @context = context
59
+ @agents = agents || config[:agents]
60
+ @timeout = timeout
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
65
+ @hooks = config[:hooks].dup
66
+ @stream = stream
67
+ @agent_event_stream = agent_event_stream
68
+ @tribunal_event_stream = tribunal_event_stream
69
+ @results = []
70
+ @errors = []
71
+ @verdict = nil
72
+ @execution_time = nil
113
73
  @agent_execution_times = []
114
74
  end
115
75
 
116
- # Instance-level hook registration — overrides class-level hooks.
117
- # :before_verdict is a transform hook: its return value replaces the results array.
118
- def on(event, &block)
119
- unless VALID_HOOKS.include?(event)
120
- raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
121
- end
122
-
123
- @hooks[event] = block
124
- self
125
- end
126
-
127
- # Instance-level process block — overrides class-level block.
128
- def process(&block)
129
- @process_block = block
130
- self
131
- end
132
-
133
76
  # Run all agents in parallel, then compute the verdict.
134
77
  # Returns self so calls can be chained: tribunal.call.verdict
135
78
  #
@@ -143,14 +86,15 @@ module ActiveHarness
143
86
 
144
87
  started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
145
88
 
146
- futures = agents.map do |agent|
89
+ futures = agents.each_with_index.map do |agent, index|
90
+ run_hook(:before_agent, agent, index)
147
91
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
148
92
  future = Concurrent::Future.execute { agent.call }
149
93
  [future, t0]
150
94
  end
151
95
 
152
- @results = []
153
- @errors = []
96
+ @results = []
97
+ @errors = []
154
98
  @agent_execution_times = []
155
99
 
156
100
  futures.each_with_index do |(future, t0), index|
@@ -159,19 +103,19 @@ module ActiveHarness
159
103
  @agent_execution_times << { agent: agents[index].class.name, time: elapsed }
160
104
 
161
105
  if future.fulfilled?
162
- value = future.value
106
+ value = future.value
163
107
  result = value.is_a?(ActiveHarness::Agent) ? value.result : value
164
108
  @results << result
165
- run_hook(:after_agent, result)
109
+ run_hook(:after_agent, result, index)
166
110
  elsif future.incomplete?
167
111
  error = Errors::TimeoutError.new(
168
112
  "Agent #{agents[index].class.name} timed out after #{@timeout}s"
169
113
  )
170
114
  @errors << { agent: agents[index].class.name, error: error }
171
- run_hook(:agent_error, agents[index].class.name, error)
115
+ run_hook(:agent_error, agents[index].class.name, error, index)
172
116
  else
173
117
  @errors << { agent: agents[index].class.name, error: future.reason }
174
- run_hook(:agent_error, agents[index].class.name, future.reason)
118
+ run_hook(:agent_error, agents[index].class.name, future.reason, index)
175
119
  end
176
120
  end
177
121
 
@@ -179,13 +123,13 @@ module ActiveHarness
179
123
 
180
124
  run_hook(:after_call, @results, @errors)
181
125
 
182
- if @results.empty?
183
- messages = @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
184
- raise Errors::AllAgentsFailed, "All agents failed — #{messages}"
185
- end
126
+ # If all agents failed, raise an exception.
127
+ # Otherwise, compute the verdict based on successful results.
128
+ check_failure_threshold!
186
129
 
187
130
  verdict_input = transform_hook(:before_verdict, @results)
188
- @verdict = @process_block ? @process_block.call(verdict_input) : nil
131
+ @verdict = compute_verdict(verdict_input)
132
+
189
133
  run_hook(:after_verdict, @verdict)
190
134
 
191
135
  self
@@ -193,26 +137,39 @@ module ActiveHarness
193
137
 
194
138
  private
195
139
 
196
- def run_hook(event, *args)
197
- @hooks[event]&.call(*args)
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
198
147
  end
199
148
 
200
- # Like run_hook but uses the return value to replace the passed value.
201
- def transform_hook(event, value)
202
- return value unless @hooks[event]
203
-
204
- @hooks[event].call(value)
149
+ def error_summary
150
+ @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
205
151
  end
206
152
 
207
153
  def resolve_agents
208
154
  @agents.map do |agent|
209
155
  if agent.is_a?(Class)
210
- agent.new(input: @input, context: @context.dup)
156
+ agent.new(
157
+ input: @input,
158
+ context: @context.dup,
159
+ stream: @stream,
160
+ event_stream: @agent_event_stream
161
+ )
211
162
  else
212
- agent.input = @input if @input
163
+ agent.input = @input if @input
164
+ agent.stream = @stream if @stream
165
+ agent.event_stream = @agent_event_stream if @agent_event_stream
213
166
  agent
214
167
  end
215
168
  end
216
169
  end
217
170
  end
218
171
  end
172
+
173
+ require_relative "tribunal/hooks"
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.13
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher
@@ -72,6 +72,9 @@ files:
72
72
  - lib/active_harness/railtie.rb
73
73
  - lib/active_harness/result.rb
74
74
  - lib/active_harness/tribunal.rb
75
+ - lib/active_harness/tribunal/dsl.rb
76
+ - lib/active_harness/tribunal/hooks.rb
77
+ - lib/active_harness/tribunal/processing.rb
75
78
  - lib/generators/active_harness/agent/agent_generator.rb
76
79
  - lib/generators/active_harness/agent/templates/agent.rb.tt
77
80
  - lib/generators/active_harness/install/install_generator.rb