Class: ActiveHarness::Agent

Inherits:
Object
  • Object
show all
Includes:
Core::HookRunner
Defined in:
lib/active_harness/agent.rb,
lib/active_harness/agent/cost.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,
lib/active_harness/agent/custom_llm_backend.rb

Direct Known Subclasses

SupportAgent, SupportGuardAgent

Defined Under Namespace

Classes: BackendParams

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. InvalidRequestError is included here so that a bad model name (or any per-model request failure) does not abort the entire chain — the next fallback model will be attempted instead.

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

Errors that abort the entire chain immediately. InvalidApiKeyError — the key is wrong for every model, retrying is pointless. SafetyBlockedError — the input itself is blocked; a different model won’t help.

[
  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 },
  xai:         -> { Providers::XAI.new },
  deepseek:    -> { Providers::DeepSeek.new },
  mistral:     -> { Providers::Mistral.new },
  ollama:      -> { Providers::Ollama.new },
  perplexity:  -> { Providers::Perplexity.new },
  gpustack:    -> { Providers::GPUStack.new },
  azure:       -> { Providers::Azure.new },
  bedrock:     -> { Providers::Bedrock.new },
  vertexai:    -> { Providers::VertexAI.new },
  custom:      -> { Providers::Custom.new }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Agent.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/active_harness/agent.rb', line 66

def initialize(
  input:   nil,
  context: {},
  params:  {},
  memory:  nil,
  models:  nil,
  streams: {}
)
  @input           = input
  @config          = self.class.agent_config
  normalize_input!
  @context         = context
  @params          = params
  @memory          = memory
  @models_override  = Array(models) if models
  @context_window   = lookup_context_window(self.models.to_a.first)
  @token_stream     = streams[:token]
  @event_stream     = streams[:agent]
  fire(:setup)
end

Instance Attribute Details

#contextObject


Instance API




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

def context
  @context
end

#context_windowObject (readonly)

Returns the value of attribute context_window.



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

def context_window
  @context_window
end

#event_streamObject (readonly)

Returns the value of attribute event_stream.



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

def event_stream
  @event_stream
end

#inputObject


Instance API




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

def input
  @input
end

#memoryObject


Instance API




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

def memory
  @memory
end

#paramsObject


Instance API




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

def params
  @params
end

#resultObject (readonly)

Returns the value of attribute result.



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

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

#token_streamObject (readonly)

Returns the value of attribute token_stream.



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

def token_stream
  @token_stream
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.



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

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: {}, params: {}, memory: nil, models: nil, streams: {}) ⇒ Object

Class-level entry point.

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


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/active_harness/agent.rb', line 13

def call(
  input:   nil,
  context: {},
  params:  {},
  memory:  nil,
  models:  nil,
  streams: {}
)
  new(
    input:   input,
    context: context,
    params:  params,
    memory:  memory,
    models:  models,
    streams: streams
  ).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

.custom_llm_backend(&block) ⇒ Object

Define the custom LLM backend block for this agent class.



35
36
37
# File 'lib/active_harness/agent/custom_llm_backend.rb', line 35

def custom_llm_backend(&block)
  agent_config[:custom_llm_backend] = block
end

.format(type) ⇒ Object

Output format for this agent.

format :text   # default — output is returned as-is
format :json   # output is parsed; result.processed 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



36
37
38
# File 'lib/active_harness/agent.rb', line 36

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

.normalize_input(value = true) ⇒ Object

Automatically strip and collapse whitespace in @input before each call. Enabled by default. Disable with:

normalize_input false


44
45
46
# File 'lib/active_harness/agent.rb', line 44

def normalize_input(value = true)
  agent_config[:normalize_input] = value
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, streams: 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 })


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
# File 'lib/active_harness/agent.rb', line 93

def call(input = nil, streams: nil)
  if input
    @input = input
    normalize_input!
  end
  if streams
    @token_stream = streams[:token] if streams.key?(:token)
    @event_stream = streams[:agent] if streams.key?(:agent)
  end
  fire(:before_call)
  @system_prompt = resolve_system_prompt
  attempts = []

  cfg = ActiveHarness.config

  model_list.each do |entry|
    retry_policy = Http::RetryPolicy.new(
      max_attempts: entry[:retry_attempts] || cfg.retry_default_attempts,
      base_delay:   entry[:retry_delay]    || cfg.retry_default_delay
    )
    t0       = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    response = retry_policy.run { attempt_model(entry, @system_prompt) }
    elapsed  = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    result   = build_result(response, entry, attempts, elapsed)
    fire(:after_call, result)
    @result = result
    return self
  rescue *RETRYABLE_ERRORS => e
    elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    attempts << attempt_entry(entry, e, elapsed)
    fire(:retry, entry, e)
    next
  rescue *STOP_ERRORS => e
    elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
    attempts << attempt_entry(entry, e, elapsed)
    fire(:retry, entry, e)
    raise
  end

  fire(: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

#models=(list) ⇒ Object



61
62
63
64
# File 'lib/active_harness/agent.rb', line 61

def models=(list)
  @models_override = Array(list)
  @model_list_proxy = nil
end