Class: ActiveHarness::Tribunal
- Inherits:
-
Object
- Object
- ActiveHarness::Tribunal
- Defined in:
- lib/active_harness/tribunal.rb,
lib/active_harness/tribunal/dsl.rb,
lib/active_harness/tribunal/hooks.rb,
lib/active_harness/tribunal/processing.rb
Overview
Can be used directly or subclassed with a class-level DSL.
Direct usage:
tribunal = ActiveHarness::Tribunal.new(
input: "Is this message toxic?",
context: { user_id: 42 },
agents: [ToxicityAgent, BiasAgent, SpamAgent],
timeout: 7
)
tribunal.on(:after_agent) { |result| puts result.model }
tribunal.process { |results| results.all? { |r| r.parsed["result"] == true } }
tribunal.call
Subclass with DSL:
class ContentQualityTribunal < ActiveHarness::Tribunal
agents PolitenessAgent, ConstructivenessAgent
on(:after_agent) { |result| puts result.model }
process { |results| results.all? { |r| r.parsed["result"] == true } }
end
ContentQualityTribunal.new(input: "...").call
Direct Known Subclasses
Constant Summary collapse
- VALID_STRATEGIES =
Declarative verdict — built-in aggregation strategy with a per-result evaluator.
Strategies:
:unanimous — verdict true when every successful result evaluates to true :majority — verdict true when more than half of successful results evaluate to trueOptions:
may_fail: N — tolerate up to N agent errors before raising AllAgentsFailed (default: nil — raise only when all agents fail, preserving legacy behavior)The block receives a single Result and must return a truthy/falsy value.
verdict :unanimous do |result| result.parsed["result"] == true end verdict :majority, may_fail: 1 do |result| result.parsed["result"] == true end %i[unanimous majority].freeze
- VALID_HOOKS =
%i[ before_call before_agent after_agent agent_error after_call before_verdict after_verdict ].freeze
Instance Attribute Summary collapse
-
#agent_event_stream ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#agent_execution_times ⇒ Object
readonly
Returns the value of attribute agent_execution_times.
-
#context ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
-
#execution_time ⇒ Object
readonly
Returns the value of attribute execution_time.
-
#input ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#results ⇒ Object
readonly
Returns the value of attribute results.
-
#stream ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#tribunal_event_stream ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#verdict ⇒ Object
readonly
Returns the value of attribute verdict.
Class Method Summary collapse
- .after(event, &block) ⇒ Object
-
.agents(*list) ⇒ Object
Declare agents at the class level.
-
.before(event, &block) ⇒ Object
Rails-style aliases for
on:. - .callback(event, &block) ⇒ Object
- .inherited(subclass) ⇒ Object
-
.on(event, &block) ⇒ Object
Class-level hook registration.
-
.process(&block) ⇒ Object
Class-level process block — defines how to compute the verdict from all results.
-
.tribunal_config ⇒ Object
Each subclass gets its own isolated config hash.
- .verdict(strategy, may_fail: nil, &block) ⇒ Object
Instance Method Summary collapse
-
#call ⇒ Object
Run all agents in parallel, then compute the verdict.
-
#initialize(input: nil, context: {}, agents: nil, timeout: 7, stream: nil, agent_event_stream: nil, tribunal_event_stream: nil, may_fail: :_unset) ⇒ Tribunal
constructor
A new instance of Tribunal.
-
#on(event, &block) ⇒ Object
Instance-level hook registration — overrides class-level hooks for this instance.
-
#process(&block) ⇒ Object
Instance-level process block — overrides class-level block.
Constructor Details
#initialize(input: nil, context: {}, agents: nil, timeout: 7, stream: nil, agent_event_stream: nil, tribunal_event_stream: nil, may_fail: :_unset) ⇒ Tribunal
Returns a new instance of Tribunal.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/active_harness/tribunal.rb', line 52 def initialize(input: nil, context: {}, agents: nil, timeout: 7, stream: nil, agent_event_stream: nil, tribunal_event_stream: nil, may_fail: :_unset) config = self.class.tribunal_config @input = input @context = context @agents = agents || config[:agents] @timeout = timeout @process_block = config[:process] @strategy = config[:strategy] @evaluate_block = config[:evaluate_block] @may_fail = may_fail == :_unset ? config[:may_fail] : may_fail @hooks = config[:hooks].dup @stream = stream @agent_event_stream = agent_event_stream @tribunal_event_stream = tribunal_event_stream @results = [] @errors = [] @verdict = nil @execution_time = nil @agent_execution_times = [] end |
Instance Attribute Details
#agent_event_stream ⇒ Object
Instance API
49 50 51 |
# File 'lib/active_harness/tribunal.rb', line 49 def agent_event_stream @agent_event_stream end |
#agent_execution_times ⇒ Object (readonly)
Returns the value of attribute agent_execution_times.
50 51 52 |
# File 'lib/active_harness/tribunal.rb', line 50 def agent_execution_times @agent_execution_times end |
#context ⇒ Object
Instance API
49 50 51 |
# File 'lib/active_harness/tribunal.rb', line 49 def context @context end |
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
50 51 52 |
# File 'lib/active_harness/tribunal.rb', line 50 def errors @errors end |
#execution_time ⇒ Object (readonly)
Returns the value of attribute execution_time.
50 51 52 |
# File 'lib/active_harness/tribunal.rb', line 50 def execution_time @execution_time end |
#input ⇒ Object
Instance API
49 50 51 |
# File 'lib/active_harness/tribunal.rb', line 49 def input @input end |
#results ⇒ Object (readonly)
Returns the value of attribute results.
50 51 52 |
# File 'lib/active_harness/tribunal.rb', line 50 def results @results end |
#stream ⇒ Object
Instance API
49 50 51 |
# File 'lib/active_harness/tribunal.rb', line 49 def stream @stream end |
#tribunal_event_stream ⇒ Object
Instance API
49 50 51 |
# File 'lib/active_harness/tribunal.rb', line 49 def tribunal_event_stream @tribunal_event_stream end |
#verdict ⇒ Object (readonly)
Returns the value of attribute verdict.
50 51 52 |
# File 'lib/active_harness/tribunal.rb', line 50 def verdict @verdict end |
Class Method Details
.after(event, &block) ⇒ Object
45 46 47 |
# File 'lib/active_harness/tribunal/hooks.rb', line 45 def after(event, &block) on(:"after_#{event}", &block) end |
.agents(*list) ⇒ Object
Declare agents at the class level.
agents PolitenessAgent, ConstructivenessAgent
agents [PolitenessAgent, ConstructivenessAgent]
8 9 10 |
# File 'lib/active_harness/tribunal/dsl.rb', line 8 def agents(*list) tribunal_config[:agents] = list.flatten end |
.before(event, &block) ⇒ Object
Rails-style aliases for on:
before :call do ... end # → on :before_call
before :agent do |agent| end # → on :before_agent
before :verdict do |results| end # → on :before_verdict (transform)
after :call do |r, e| end # → on :after_call
after :agent do |result| end # → on :after_agent
after :verdict do |verdict| end # → on :after_verdict
callback :agent_error do |name, e| end # → on :agent_error
41 42 43 |
# File 'lib/active_harness/tribunal/hooks.rb', line 41 def before(event, &block) on(:"before_#{event}", &block) end |
.callback(event, &block) ⇒ Object
49 50 51 |
# File 'lib/active_harness/tribunal/hooks.rb', line 49 def callback(event, &block) on(event, &block) end |
.inherited(subclass) ⇒ Object
41 42 43 |
# File 'lib/active_harness/tribunal.rb', line 41 def inherited(subclass) subclass.instance_variable_set(:@tribunal_config, { agents: [], hooks: {} }) end |
.on(event, &block) ⇒ Object
Class-level hook registration.
on :before_call do ... end
on :before_agent do |agent| ... end
on :after_agent do |result| ... end
on :agent_error do |name, error| ... end
on :after_call do |results, errors| ... end
on :before_verdict do |results| results end # transform hook
on :after_verdict do |verdict| ... end
23 24 25 26 27 28 29 30 |
# File 'lib/active_harness/tribunal/hooks.rb', line 23 def on(event, &block) unless VALID_HOOKS.include?(event) raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}" end tribunal_config[:hooks][event] = block end |
.process(&block) ⇒ Object
Class-level process block — defines how to compute the verdict from all results. Receives the full results array; return value becomes #verdict. Takes priority over verdict strategy if both are declared.
process { |results| results.all? { |r| r.parsed["result"] == true } }
17 18 19 |
# File 'lib/active_harness/tribunal/dsl.rb', line 17 def process(&block) tribunal_config[:process] = block end |
.tribunal_config ⇒ Object
Each subclass gets its own isolated config hash.
37 38 39 |
# File 'lib/active_harness/tribunal.rb', line 37 def tribunal_config @tribunal_config ||= { agents: [], hooks: {} } end |
.verdict(strategy, may_fail: nil, &block) ⇒ Object
42 43 44 45 46 47 48 49 50 51 |
# File 'lib/active_harness/tribunal/dsl.rb', line 42 def verdict(strategy, may_fail: nil, &block) unless VALID_STRATEGIES.include?(strategy) raise ArgumentError, "Unknown verdict strategy :#{strategy}. Valid strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(", ")}" end tribunal_config[:strategy] = strategy tribunal_config[:may_fail] = may_fail tribunal_config[:evaluate_block] = block end |
Instance Method Details
#call ⇒ Object
Run all agents in parallel, then compute the verdict. Returns self so calls can be chained: tribunal.call.verdict
Behaviour on failure:
- If some agents fail/timeout, their errors are in #errors and
#results contains only successful results.
- If ALL agents fail/timeout, raises Errors::AllAgentsFailed.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/active_harness/tribunal.rb', line 83 def call agents = resolve_agents run_hook(:before_call) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) futures = agents.each_with_index.map do |agent, index| run_hook(:before_agent, agent, index) t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) future = Concurrent::Future.execute { agent.call } [future, t0] end @results = [] @errors = [] @agent_execution_times = [] futures.each_with_index do |(future, t0), index| future.wait(@timeout) elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3) @agent_execution_times << { agent: agents[index].class.name, time: elapsed } if future.fulfilled? value = future.value result = value.is_a?(ActiveHarness::Agent) ? value.result : value @results << result run_hook(:after_agent, result, index) elsif future.incomplete? error = Errors::TimeoutError.new( "Agent #{agents[index].class.name} timed out after #{@timeout}s" ) @errors << { agent: agents[index].class.name, error: error } run_hook(:agent_error, agents[index].class.name, error, index) else @errors << { agent: agents[index].class.name, error: future.reason } run_hook(:agent_error, agents[index].class.name, future.reason, index) end end @execution_time = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at).round(3) run_hook(:after_call, @results, @errors) # If all agents failed, raise an exception. # Otherwise, compute the verdict based on successful results. check_failure_threshold! verdict_input = transform_hook(:before_verdict, @results) @verdict = compute_verdict(verdict_input) run_hook(:after_verdict, @verdict) self end |
#on(event, &block) ⇒ Object
Instance-level hook registration — overrides class-level hooks for this instance. :before_verdict is a transform hook: its return value replaces the results array passed to the process block.
57 58 59 60 61 62 63 64 65 |
# File 'lib/active_harness/tribunal/hooks.rb', line 57 def on(event, &block) unless VALID_HOOKS.include?(event) raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}" end @hooks[event] = block self end |
#process(&block) ⇒ Object
Instance-level process block — overrides class-level block.
tribunal.process { |results| results.count { |r| r.parsed["ok"] } >= 2 }
6 7 8 9 |
# File 'lib/active_harness/tribunal/processing.rb', line 6 def process(&block) @process_block = block self end |