Class: ActiveHarness::Tribunal

Inherits:
Object
  • Object
show all
Defined in:
lib/active_harness/tribunal.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

TestSupportGuardTribunal

Constant Summary collapse

VALID_HOOKS =
%i[
  before_call
  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) ⇒ Tribunal

Returns a new instance of Tribunal.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/active_harness/tribunal.rb', line 100

def initialize(input: nil, context: {}, agents: nil, timeout: 7)
  config = self.class.tribunal_config

  @input         = input
  @context       = context
  @agents        = agents || config[:agents]
  @timeout       = timeout
  @process_block = config[:process]
  @hooks         = config[:hooks].dup
  @results       = []
  @errors        = []
  @verdict       = nil
  @execution_time       = nil
  @agent_execution_times = []
end

Instance Attribute Details

#agent_execution_timesObject (readonly)

Returns the value of attribute agent_execution_times.



98
99
100
# File 'lib/active_harness/tribunal.rb', line 98

def agent_execution_times
  @agent_execution_times
end

#errorsObject (readonly)

Returns the value of attribute errors.



98
99
100
# File 'lib/active_harness/tribunal.rb', line 98

def errors
  @errors
end

#execution_timeObject (readonly)

Returns the value of attribute execution_time.



98
99
100
# File 'lib/active_harness/tribunal.rb', line 98

def execution_time
  @execution_time
end

#inputObject

Returns the value of attribute input.



97
98
99
# File 'lib/active_harness/tribunal.rb', line 97

def input
  @input
end

#resultsObject (readonly)

Returns the value of attribute results.



98
99
100
# File 'lib/active_harness/tribunal.rb', line 98

def results
  @results
end

#verdictObject (readonly)

Returns the value of attribute verdict.



98
99
100
# File 'lib/active_harness/tribunal.rb', line 98

def verdict
  @verdict
end

Class Method Details

.after(event, &block) ⇒ Object



74
75
76
# File 'lib/active_harness/tribunal.rb', line 74

def after(event, &block)
  on(:"after_#{event}", &block)
end

.agents(*list) ⇒ Object

Declare agents at the class level.

agents PolitenessAgent, ConstructivenessAgent


47
48
49
# File 'lib/active_harness/tribunal.rb', line 47

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 ... end   # → on :before_agent (not used yet)
before :verdict                  do |r| end   # → on :before_verdict
after  :call                     do ... end   # → on :after_call
after  :agent                    do |r| end   # → on :after_agent
after  :verdict                  do |v| end   # → on :after_verdict
callback :agent_error            do |n,e| end # → on :agent_error


70
71
72
# File 'lib/active_harness/tribunal.rb', line 70

def before(event, &block)
  on(:"before_#{event}", &block)
end

.callback(event, &block) ⇒ Object



78
79
80
# File 'lib/active_harness/tribunal.rb', line 78

def callback(event, &block)
  on(event, &block)
end

.inherited(subclass) ⇒ Object



92
93
94
# File 'lib/active_harness/tribunal.rb', line 92

def inherited(subclass)
  subclass.instance_variable_set(:@tribunal_config, { agents: [], hooks: {} })
end

.on(event, &block) ⇒ Object

Class-level hook registration.

on(:after_agent) { |result| puts result.model }


53
54
55
56
57
58
59
# File 'lib/active_harness/tribunal.rb', line 53

def on(event, &block)
  unless VALID_HOOKS.include?(event)
    raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
  end

  tribunal_config[:hooks][event] = block
end

.process(&block) ⇒ Object

Class-level process block.

process { |results| results.all? { |r| r.parsed["result"] == true } }


84
85
86
# File 'lib/active_harness/tribunal.rb', line 84

def process(&block)
  tribunal_config[:process] = block
end

.tribunal_configObject



88
89
90
# File 'lib/active_harness/tribunal.rb', line 88

def tribunal_config
  @tribunal_config ||= { agents: [], hooks: {} }
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.


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/active_harness/tribunal.rb', line 140

def call
  agents = resolve_agents
  run_hook(:before_call)

  started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  futures = agents.map do |agent|
    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?
      @results << future.value
      run_hook(:after_agent, future.value)
    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)
    else
      @errors << { agent: agents[index].class.name, error: future.reason }
      run_hook(:agent_error, agents[index].class.name, future.reason)
    end
  end

  @execution_time = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at).round(3)

  run_hook(:after_call, @results, @errors)

  if @results.empty?
    messages = @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
    raise Errors::AllAgentsFailed, "All agents failed — #{messages}"
  end

  verdict_input = transform_hook(:before_verdict, @results)
  @verdict = @process_block ? @process_block.call(verdict_input) : nil
  run_hook(:after_verdict, @verdict)

  self
end

#on(event, &block) ⇒ Object

Instance-level hook registration — overrides class-level hooks. :before_verdict is a transform hook: its return value replaces the results array.



118
119
120
121
122
123
124
125
# File 'lib/active_harness/tribunal.rb', line 118

def on(event, &block)
  unless VALID_HOOKS.include?(event)
    raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
  end

  @hooks[event] = block
  self
end

#process(&block) ⇒ Object

Instance-level process block — overrides class-level block.



128
129
130
131
# File 'lib/active_harness/tribunal.rb', line 128

def process(&block)
  @process_block = block
  self
end