Class: ActiveHarness::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/active_harness/agent.rb,
lib/active_harness/agent/hooks.rb,
lib/active_harness/agent/models.rb,
lib/active_harness/agent/prompt.rb,
lib/active_harness/agent/providers.rb,
lib/active_harness/agent/output_parser.rb

Direct Known Subclasses

TestSupportAgent, TestSupportGuardAgent

Constant Summary collapse

VALID_HOOKS =
%i[
  setup
  before_call
  after_call
  before_system_prompt
  after_system_prompt
  before_parse
  after_parse
  parse_error
  retry
  failure
].freeze
RETRYABLE_ERRORS =

Errors that allow retrying the next model in the chain

[
  Errors::TimeoutError,
  Errors::RateLimitError,
  Errors::ServerError,
  Errors::ProviderUnavailableError
].freeze
STOP_ERRORS =

Errors that abort the entire chain immediately

[
  Errors::InvalidRequestError,
  Errors::InvalidApiKeyError,
  Errors::SafetyBlockedError
].freeze
PROVIDERS =
{
  openai:     -> { Providers::OpenAI.new },
  openrouter: -> { Providers::OpenRouter.new },
  groq:       -> { Providers::Groq.new },
  gemini:     -> { Providers::Gemini.new },
  anthropic:  -> { Providers::Anthropic.new }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input: nil, context: {}, models: nil, memory: nil, stream: nil) ⇒ Agent

Returns a new instance of Agent.



34
35
36
37
38
39
40
41
42
43
# File 'lib/active_harness/agent.rb', line 34

def initialize(input: nil, context: {}, models: nil, memory: nil, stream: nil)
  @input           = input
  @context         = context
  @config          = self.class.agent_config
  @models_override = Array(models) if models
  @stream          = stream
  # memory: can be passed directly or via context[:memory]
  @memory = memory || @context[:memory]
  run_hook(:setup)
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



32
33
34
# File 'lib/active_harness/agent.rb', line 32

def context
  @context
end

#inputObject


Instance API




31
32
33
# File 'lib/active_harness/agent.rb', line 31

def input
  @input
end

#resultObject (readonly)

Returns the value of attribute result.



32
33
34
# File 'lib/active_harness/agent.rb', line 32

def result
  @result
end

#system_promptObject (readonly)

Returns the value of attribute system_prompt.



21
22
23
# File 'lib/active_harness/agent/prompt.rb', line 21

def system_prompt
  @system_prompt
end

Class Method Details

.after(event, &block) ⇒ Object



53
54
55
# File 'lib/active_harness/agent/hooks.rb', line 53

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

.agent_configObject

Each subclass gets its own isolated config hash.



19
20
21
# File 'lib/active_harness/agent.rb', line 19

def agent_config
  @agent_config ||= {}
end

.before(event, &block) ⇒ Object

Rails-style aliases for on:

before :call                    do ... end  # → on :before_call
before :system_prompt           do ... end  # → on :before_system_prompt
after  :call                    do |r| end  # → on :after_call
after  :system_prompt           do |p| end  # → on :after_system_prompt
after  :parse                   do |p| end  # → on :after_parse
callback :retry                 do |e,err| end
callback :failure               do |a| end
callback :setup                 do end
callback :parse_error           do |r,e| end


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

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

.call(input: nil, context: {}, models: nil, memory: nil, stream: nil) ⇒ Object

Class-level entry point.

SupportAgent.call(input: "Hi")
SupportAgent.call(input: "Hi", context: { user_id: 42 })
SupportAgent.call(input: "Hi", memory: memory)


14
15
16
# File 'lib/active_harness/agent.rb', line 14

def call(input: nil, context: {}, models: nil, memory: nil, stream: nil)
  new(input: input, context: context, models: models, memory: memory, stream: stream).call
end

.callback(event, &block) ⇒ Object



57
58
59
# File 'lib/active_harness/agent/hooks.rb', line 57

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

.format(type) ⇒ Object

Output format for this agent.

format :text   # default — output is returned as-is
format :json   # output is parsed; result.parsed is a Ruby Hash/Array


18
19
20
21
22
23
24
# File 'lib/active_harness/agent/models.rb', line 18

def format(type)
  unless %i[text json].include?(type)
    raise ArgumentError, "Unknown format :#{type}. Valid values: :text, :json"
  end

  agent_config[:format] = type
end

.inherited(subclass) ⇒ Object



23
24
25
# File 'lib/active_harness/agent.rb', line 23

def inherited(subclass)
  subclass.instance_variable_set(:@agent_config, {})
end

.model(&block) ⇒ Object

Block-based model DSL:

model do
  use      provider: :openrouter, model: "mistralai/mistral-nemo"
  fallback provider: :openrouter, model: "meta-llama/llama-3.3-70b-instruct:free"
end


32
33
34
35
36
# File 'lib/active_harness/agent/models.rb', line 32

def model(&block)
  config = ModelConfig.new
  config.instance_eval(&block)
  agent_config[:model] = config.to_h
end

.models(list) ⇒ Object

Array-style model list:

models [
  { provider: :openai,     model: "gpt-4.1-mini" },
  { provider: :openrouter, model: "mistralai/mistral-nemo" }
]


10
11
12
# File 'lib/active_harness/agent/models.rb', line 10

def models(list)
  agent_config[:models] = Array(list)
end

.on(event, &block) ⇒ Object

Unified hook DSL.

on :setup                do ... end
on :before_call          do ... end
on :after_call           do |result| ... end
on :before_system_prompt do ... end
on :after_system_prompt  do |prompt| ... end
on :before_parse         do |raw| ... end
on :after_parse          do |parsed| ... end
on :parse_error          do |raw, error| ... end
on :retry                do |entry, error| ... end
on :failure              do |attempts| ... end


29
30
31
32
33
34
35
36
# File 'lib/active_harness/agent/hooks.rb', line 29

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

  agent_config[:hooks] ||= {}
  agent_config[:hooks][event] = block
end

.system_prompt(text_or_class) ⇒ Object Also known as: prompt

System prompt for this agent. Accepts:

- a String  → used as-is
- a Class   → instantiated and resolved via #call or #text
- a Proc    → called at request time (no arguments)

system_prompt "You are a helpful assistant."
system_prompt MyPromptClass
system_prompt -> { "Dynamic prompt built at #{Time.now}" }


13
14
15
# File 'lib/active_harness/agent/prompt.rb', line 13

def system_prompt(text_or_class)
  agent_config[:system_prompt] = text_or_class
end

Instance Method Details

#call(input = nil, stream: nil) ⇒ Object

Attempts each model in order, returns the first successful Result. Raises Errors::AllModelsFailed if every model in the chain fails.

Optionally accepts input and stream callback inline:

agent.call("What is the capital of Japan?")
agent.call("...", stream: ->(token) { print token })


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/active_harness/agent.rb', line 51

def call(input = nil, stream: nil)
  @input  = input  if input
  @stream = stream if stream
  @memory&.load
  @system_prompt = resolve_system_prompt
  run_hook(:before_call)
  attempts = []

  model_list.each do |entry|
    t0       = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    response = attempt_model(entry, @system_prompt)
    elapsed  = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    result   = build_result(response, entry, attempts, elapsed)
    save_to_memory(result)
    run_hook(:after_call, result)
    @result = result
    return result
  rescue *RETRYABLE_ERRORS => e
    elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    attempts << attempt_entry(entry, e, elapsed)
    run_hook(:retry, entry, e)
    next
  rescue *STOP_ERRORS => e
    elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    attempts << attempt_entry(entry, e, elapsed)
    run_hook(:retry, entry, e)
    raise
  end

  run_hook(:failure, attempts)
  raise Errors::AllModelsFailed, "All models failed. Attempts: #{attempts.inspect}"
end

#modelsObject

Public instance API — returns a ModelList proxy for this agent instance.

Allows adding models at runtime before calling the agent:

agent.models.prepend([{ provider: :openrouter, model: "..." }])
agent.models.push([{ provider: :openrouter, model: "..." }])

Any prepended/pushed models are combined with the class-defined chain:

[prepended...] + [class chain / constructor override] + [pushed...]


48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/active_harness/agent/models.rb', line 48

def models
  @model_list_proxy ||= begin
    base = if @models_override&.any?
             @models_override
           elsif (m = @config[:model])
             m[:models] || []
           else
             @config[:models] || []
           end
    ModelList.new(base)
  end
end