Class: ActiveHarness::Tribunal

Inherits:
Object
  • Object
show all
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

SupportGuardTribunal

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 true

Options:

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

Class Method Summary collapse

Instance Method Summary collapse

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_streamObject


Instance API




49
50
51
# File 'lib/active_harness/tribunal.rb', line 49

def agent_event_stream
  @agent_event_stream
end

#agent_execution_timesObject (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

#contextObject


Instance API




49
50
51
# File 'lib/active_harness/tribunal.rb', line 49

def context
  @context
end

#errorsObject (readonly)

Returns the value of attribute errors.



50
51
52
# File 'lib/active_harness/tribunal.rb', line 50

def errors
  @errors
end

#execution_timeObject (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

#inputObject


Instance API




49
50
51
# File 'lib/active_harness/tribunal.rb', line 49

def input
  @input
end

#resultsObject (readonly)

Returns the value of attribute results.



50
51
52
# File 'lib/active_harness/tribunal.rb', line 50

def results
  @results
end

#streamObject


Instance API




49
50
51
# File 'lib/active_harness/tribunal.rb', line 49

def stream
  @stream
end

#tribunal_event_streamObject


Instance API




49
50
51
# File 'lib/active_harness/tribunal.rb', line 49

def tribunal_event_stream
  @tribunal_event_stream
end

#verdictObject (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_configObject

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

#callObject

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