Class: ActiveHarness::Agent
- Inherits:
-
Object
- Object
- ActiveHarness::Agent
- 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
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
-
#context ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#context_window ⇒ Object
readonly
Returns the value of attribute context_window.
-
#event_stream ⇒ Object
readonly
Returns the value of attribute event_stream.
-
#input ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#memory ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#params ⇒ Object
————————————————————————- Instance API ————————————————————————-.
-
#result ⇒ Object
readonly
Returns the value of attribute result.
-
#system_prompt ⇒ Object
readonly
Returns the value of attribute system_prompt.
-
#token_stream ⇒ Object
readonly
Returns the value of attribute token_stream.
Class Method Summary collapse
- .after(event, &block) ⇒ Object
-
.agent_config ⇒ Object
Each subclass gets its own isolated config hash.
-
.before(event, &block) ⇒ Object
Rails-style aliases for
on:. -
.call(input: nil, context: {}, params: {}, memory: nil, models: nil, streams: {}) ⇒ Object
Class-level entry point.
- .callback(event, &block) ⇒ Object
-
.custom_llm_backend(&block) ⇒ Object
Define the custom LLM backend block for this agent class.
-
.format(type) ⇒ Object
Output format for this agent.
- .inherited(subclass) ⇒ Object
-
.model(&block) ⇒ Object
Block-based model DSL:.
-
.models(list) ⇒ Object
Array-style model list:.
-
.normalize_input(value = true) ⇒ Object
Automatically strip and collapse whitespace in @input before each call.
-
.on(event, &block) ⇒ Object
Unified hook DSL.
-
.system_prompt(text_or_class) ⇒ Object
(also: prompt)
System prompt for this agent.
Instance Method Summary collapse
-
#call(input = nil, streams: nil) ⇒ Object
Attempts each model in order, returns the first successful Result.
-
#initialize(input: nil, context: {}, params: {}, memory: nil, models: nil, streams: {}) ⇒ Agent
constructor
A new instance of Agent.
-
#models ⇒ Object
Public instance API — returns a ModelList proxy for this agent instance.
- #models=(list) ⇒ Object
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
#context ⇒ Object
Instance API
52 53 54 |
# File 'lib/active_harness/agent.rb', line 52 def context @context end |
#context_window ⇒ Object (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_stream ⇒ Object (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 |
#input ⇒ Object
Instance API
52 53 54 |
# File 'lib/active_harness/agent.rb', line 52 def input @input end |
#memory ⇒ Object
Instance API
52 53 54 |
# File 'lib/active_harness/agent.rb', line 52 def memory @memory end |
#params ⇒ Object
Instance API
52 53 54 |
# File 'lib/active_harness/agent.rb', line 52 def params @params end |
#result ⇒ Object (readonly)
Returns the value of attribute result.
56 57 58 |
# File 'lib/active_harness/agent.rb', line 56 def result @result end |
#system_prompt ⇒ Object (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_stream ⇒ Object (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_config ⇒ Object
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 |
#models ⇒ Object
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 |