active_harness 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_harness/agent/hooks.rb +75 -0
  3. data/lib/active_harness/agent/models.rb +147 -0
  4. data/lib/active_harness/agent/output_parser.rb +57 -0
  5. data/lib/active_harness/agent/prompt.rb +58 -0
  6. data/lib/active_harness/agent/providers.rb +54 -0
  7. data/lib/active_harness/agent.rb +107 -228
  8. data/lib/active_harness/core/errors.rb +22 -28
  9. data/lib/active_harness/http/client.rb +8 -19
  10. data/lib/active_harness/http/streaming_client.rb +60 -0
  11. data/lib/active_harness/memory/adapter/base.rb +36 -0
  12. data/lib/active_harness/memory/adapter/file.rb +141 -0
  13. data/lib/active_harness/memory.rb +212 -0
  14. data/lib/active_harness/pipeline/step.rb +36 -0
  15. data/lib/active_harness/pipeline.rb +207 -0
  16. data/lib/active_harness/providers/PROVIDER_CONTRACT.md +54 -0
  17. data/lib/active_harness/providers/anthropic.rb +76 -4
  18. data/lib/active_harness/providers/base.rb +41 -13
  19. data/lib/active_harness/providers/gemini.rb +61 -0
  20. data/lib/active_harness/providers/groq.rb +64 -0
  21. data/lib/active_harness/providers/openai.rb +39 -47
  22. data/lib/active_harness/providers/openrouter.rb +40 -54
  23. data/lib/active_harness/railtie.rb +12 -0
  24. data/lib/active_harness/result.rb +10 -0
  25. data/lib/active_harness/tribunal.rb +216 -0
  26. data/lib/active_harness.rb +17 -46
  27. data/lib/generators/active_harness/agent/agent_generator.rb +16 -0
  28. data/lib/generators/active_harness/agent/templates/agent.rb.tt +8 -0
  29. data/lib/generators/active_harness/install/install_generator.rb +54 -0
  30. data/lib/generators/active_harness/install/templates/agents/test_support_agent.rb +10 -0
  31. data/lib/generators/active_harness/install/templates/agents/test_support_guard_agent.rb +11 -0
  32. data/lib/generators/active_harness/install/templates/controllers/ai_controller.rb +105 -0
  33. data/lib/generators/active_harness/install/templates/memory/test_support_memory.rb +16 -0
  34. data/lib/generators/active_harness/install/templates/pipelines/test_support_pipeline.rb +31 -0
  35. data/lib/generators/active_harness/install/templates/prompts/test_support_guard_prompt.rb +9 -0
  36. data/lib/generators/active_harness/install/templates/prompts/test_support_prompt.rb +5 -0
  37. data/lib/generators/active_harness/install/templates/tribunals/test_support_guard_tribunal.rb +11 -0
  38. data/lib/generators/active_harness/memory/memory_generator.rb +16 -0
  39. data/lib/generators/active_harness/memory/templates/memory.rb.tt +12 -0
  40. data/lib/generators/active_harness/pipeline/pipeline_generator.rb +16 -0
  41. data/lib/generators/active_harness/pipeline/templates/pipeline.rb.tt +19 -0
  42. data/lib/generators/active_harness/prompt/prompt_generator.rb +16 -0
  43. data/lib/generators/active_harness/prompt/templates/prompt.rb.tt +5 -0
  44. data/lib/generators/active_harness/tribunal/templates/tribunal.rb.tt +7 -0
  45. data/lib/generators/active_harness/tribunal/tribunal_generator.rb +16 -0
  46. metadata +42 -72
  47. data/LICENSE +0 -21
  48. data/README.md +0 -113
  49. data/lib/active_harness/core/configuration.rb +0 -55
  50. data/lib/active_harness/core/version.rb +0 -3
  51. data/lib/active_harness/http/retry_policy.rb +0 -47
  52. data/lib/active_harness/models/model_request.rb +0 -14
  53. data/lib/active_harness/models/model_response.rb +0 -13
  54. data/lib/active_harness/payload.rb +0 -47
  55. data/lib/active_harness/pipeline/engine.rb +0 -251
  56. data/lib/active_harness/pipeline/fallback_runner.rb +0 -76
  57. data/lib/active_harness/pipeline/guard_runner.rb +0 -125
  58. data/lib/active_harness/pipeline/output_parser.rb +0 -43
  59. data/lib/active_harness/pipeline/prompt_builder.rb +0 -46
  60. data/lib/active_harness/pipeline/provider_registry.rb +0 -16
  61. data/lib/active_harness/prompts/guard_system_prompt.rb +0 -33
  62. data/lib/active_harness/providers/google.rb +0 -11
  63. data/lib/active_harness/rate_limit/request_limiter.rb +0 -50
  64. data/lib/active_harness/rate_limit/risk_holdback.rb +0 -69
  65. data/lib/active_harness/results/debug_result.rb +0 -19
  66. data/lib/active_harness/results/input_result.rb +0 -27
  67. data/lib/active_harness/results/result.rb +0 -55
@@ -1,10 +1,82 @@
1
+ require "uri"
2
+
1
3
  module ActiveHarness
2
4
  module Providers
3
- # Phase 2Anthropic (Claude) adapter
5
+ # Anthropic Claudenative Messages API (not OpenAI-compatible).
6
+ # https://docs.anthropic.com/en/api/messages
4
7
  class Anthropic < Base
5
- def call(_request)
6
- # TODO: implement Anthropic Messages API
7
- raise NotImplementedError, "Anthropic adapter is planned for phase 2"
8
+ API_URL = URI("https://api.anthropic.com/v1/messages")
9
+ ANTHROPIC_VERSION = "2023-06-01"
10
+ DEFAULT_MAX_TOKENS = 1024
11
+
12
+ def call(model:, messages:, temperature: 0.7)
13
+ system_msg, chat_messages = extract_system(messages)
14
+
15
+ body = {
16
+ model: model,
17
+ max_tokens: DEFAULT_MAX_TOKENS,
18
+ temperature: temperature,
19
+ messages: chat_messages
20
+ }
21
+ body[:system] = system_msg if system_msg
22
+
23
+ raw = post_json(API_URL,
24
+ headers: {
25
+ "Content-Type" => "application/json",
26
+ "x-api-key" => api_key,
27
+ "anthropic-version" => ANTHROPIC_VERSION
28
+ },
29
+ body: body
30
+ )
31
+ data = parse!(raw)
32
+ handle_error!(data)
33
+
34
+ {
35
+ content: data.dig("content", 0, "text").to_s.strip,
36
+ provider: :anthropic,
37
+ model: data["model"] || model,
38
+ usage: extract_usage_anthropic(data)
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ # Anthropic keeps system prompt separate from the messages array.
45
+ def extract_system(messages)
46
+ system = messages.find { |m| m[:role] == "system" || m["role"] == "system" }
47
+ chat = messages.reject { |m| m[:role] == "system" || m["role"] == "system" }
48
+ .map { |m| { role: m[:role] || m["role"], content: m[:content] || m["content"] } }
49
+
50
+ system_text = system && (system[:content] || system["content"])
51
+ [system_text, chat]
52
+ end
53
+
54
+ def api_key
55
+ key = ENV["ANTHROPIC_API_KEY"].to_s
56
+ raise Errors::InvalidApiKeyError, "ANTHROPIC_API_KEY is not set" if key.empty?
57
+ key
58
+ end
59
+
60
+ def handle_error!(data)
61
+ return unless data["error"]
62
+
63
+ msg = data.dig("error", "message").to_s
64
+ type = data.dig("error", "type").to_s
65
+ metadata = data["error"].reject { |k, _| %w[message type].include?(k) }
66
+ metadata = nil if metadata.empty?
67
+
68
+ case type
69
+ when "authentication_error"
70
+ raise Errors::InvalidApiKeyError.new(msg, error_code: type, metadata: metadata)
71
+ when "rate_limit_error"
72
+ raise Errors::RateLimitError.new(msg, error_code: type, metadata: metadata)
73
+ when "overloaded_error"
74
+ raise Errors::ProviderUnavailableError.new(msg, error_code: type, metadata: metadata)
75
+ when "api_error"
76
+ raise Errors::ServerError.new(msg, error_code: type, metadata: metadata)
77
+ else
78
+ raise Errors::InvalidRequestError.new(msg, error_code: type, metadata: metadata)
79
+ end
8
80
  end
9
81
  end
10
82
  end
@@ -1,22 +1,50 @@
1
+ require "json"
2
+
1
3
  module ActiveHarness
2
4
  module Providers
3
5
  class Base
4
- # @param request [ModelRequest]
5
- # @return [ModelResponse]
6
- def call(request)
7
- raise NotImplementedError, "#{self.class}#call not implemented"
8
- end
6
+ HTTP = ActiveHarness::Http::Client.new
7
+ STREAMING_HTTP = ActiveHarness::Http::StreamingClient.new
9
8
 
10
9
  private
11
10
 
12
- def build_response(content:, provider:, model:, usage: {}, raw: nil)
13
- ModelResponse.new(
14
- content: content,
15
- provider: provider,
16
- model: model,
17
- usage: usage,
18
- raw: raw
19
- )
11
+ def post_json(url, headers:, body:, timeout: 30)
12
+ HTTP.post(url, headers: headers, body: body.to_json, timeout: timeout)
13
+ end
14
+
15
+ def post_json_stream(url, headers:, body:, timeout: 60, on_token:)
16
+ STREAMING_HTTP.post(url, headers: headers, body: body.to_json, timeout: timeout, on_token: on_token)
17
+ end
18
+
19
+ # Normalize OpenAI-compatible usage object to a consistent hash.
20
+ # Returns nil if the response contains no usage data.
21
+ def extract_usage_openai(data)
22
+ u = data["usage"]
23
+ return nil unless u
24
+ {
25
+ input_tokens: u["prompt_tokens"].to_i,
26
+ output_tokens: u["completion_tokens"].to_i,
27
+ total_tokens: u["total_tokens"].to_i
28
+ }
29
+ end
30
+
31
+ # Normalize Anthropic usage object.
32
+ def extract_usage_anthropic(data)
33
+ u = data["usage"]
34
+ return nil unless u
35
+ input = u["input_tokens"].to_i
36
+ output = u["output_tokens"].to_i
37
+ {
38
+ input_tokens: input,
39
+ output_tokens: output,
40
+ total_tokens: input + output
41
+ }
42
+ end
43
+
44
+ def parse!(raw)
45
+ JSON.parse(raw)
46
+ rescue JSON::ParserError => e
47
+ raise Errors::ProviderError, "Invalid JSON response: #{e.message}"
20
48
  end
21
49
  end
22
50
  end
@@ -0,0 +1,61 @@
1
+ require "uri"
2
+
3
+ module ActiveHarness
4
+ module Providers
5
+ # Google Gemini — OpenAI-compatible endpoint (beta).
6
+ # https://ai.google.dev/gemini-api/docs/openai
7
+ class Gemini < Base
8
+ API_URL = URI("https://generativelanguage.googleapis.com/v1beta/openai/chat/completions")
9
+
10
+ def call(model:, messages:, temperature: 0.7)
11
+ raw = post_json(API_URL,
12
+ headers: {
13
+ "Content-Type" => "application/json",
14
+ "Authorization" => "Bearer #{api_key}"
15
+ },
16
+ body: { model: model, messages: messages, temperature: temperature }
17
+ )
18
+ data = parse!(raw)
19
+ handle_error!(data)
20
+
21
+ {
22
+ content: data.dig("choices", 0, "message", "content").to_s.strip,
23
+ provider: :gemini,
24
+ model: data["model"] || model,
25
+ usage: extract_usage_openai(data)
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def api_key
32
+ key = ENV["GEMINI_API_KEY"].to_s
33
+ raise Errors::InvalidApiKeyError, "GEMINI_API_KEY is not set" if key.empty?
34
+ key
35
+ end
36
+
37
+ def handle_error!(data)
38
+ return unless data["error"]
39
+
40
+ msg = data.dig("error", "message").to_s
41
+ code = data.dig("error", "code").to_s
42
+ status = data.dig("error", "status").to_s
43
+ metadata = data["error"].reject { |k, _| %w[message code status].include?(k) }
44
+ metadata = nil if metadata.empty?
45
+
46
+ case status
47
+ when "UNAUTHENTICATED"
48
+ raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
49
+ when "RESOURCE_EXHAUSTED"
50
+ raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
51
+ when "UNAVAILABLE"
52
+ raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
53
+ when "INTERNAL"
54
+ raise Errors::ServerError.new(msg, error_code: code, metadata: metadata)
55
+ else
56
+ raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ require "uri"
2
+
3
+ module ActiveHarness
4
+ module Providers
5
+ # Groq — OpenAI-compatible API with fast inference.
6
+ # https://console.groq.com/docs/openai
7
+ class Groq < Base
8
+ API_URL = URI("https://api.groq.com/openai/v1/chat/completions")
9
+
10
+ def call(model:, messages:, temperature: 0.7)
11
+ raw = post_json(API_URL,
12
+ headers: {
13
+ "Content-Type" => "application/json",
14
+ "Authorization" => "Bearer #{api_key}"
15
+ },
16
+ body: { model: model, messages: messages, temperature: temperature }
17
+ )
18
+ data = parse!(raw)
19
+ handle_error!(data)
20
+
21
+ {
22
+ content: data.dig("choices", 0, "message", "content").to_s.strip,
23
+ provider: :groq,
24
+ model: data["model"] || model,
25
+ usage: extract_usage_openai(data)
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def api_key
32
+ key = ENV["GROQ_API_KEY"].to_s
33
+ raise Errors::InvalidApiKeyError, "GROQ_API_KEY is not set" if key.empty?
34
+ key
35
+ end
36
+
37
+ def handle_error!(data)
38
+ return unless data["error"]
39
+
40
+ msg = data.dig("error", "message").to_s
41
+ code = data.dig("error", "code").to_s
42
+ type = data.dig("error", "type").to_s
43
+ metadata = data["error"].reject { |k, _| %w[message code type].include?(k) }
44
+ metadata = nil if metadata.empty?
45
+
46
+ case code
47
+ when "invalid_api_key", "unauthorized"
48
+ raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
49
+ when "rate_limit_exceeded"
50
+ raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
51
+ when "500", "502", "503", "504"
52
+ raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
53
+ else
54
+ case type
55
+ when "server_error"
56
+ raise Errors::ServerError.new(msg, error_code: code, metadata: metadata)
57
+ else
58
+ raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,76 +1,68 @@
1
- require "json"
1
+ require "uri"
2
2
 
3
3
  module ActiveHarness
4
4
  module Providers
5
5
  class OpenAI < Base
6
6
  API_URL = URI("https://api.openai.com/v1/chat/completions")
7
7
 
8
- def call(request)
9
- body = build_body(request)
10
- raw = post(body, request.timeout)
11
- data = JSON.parse(raw)
12
-
8
+ # @param model [String]
9
+ # @param messages [Array<Hash>] [{role:, content:}, ...]
10
+ # @param temperature [Float]
11
+ # @return [Hash] { content:, provider:, model: }
12
+ def call(model:, messages:, temperature: 0.7)
13
+ raw = post_json(API_URL,
14
+ headers: {
15
+ "Content-Type" => "application/json",
16
+ "Authorization" => "Bearer #{api_key}"
17
+ },
18
+ body: { model: model, messages: messages, temperature: temperature }
19
+ )
20
+ data = parse!(raw)
13
21
  handle_error!(data)
14
22
 
15
- content = data.dig("choices", 0, "message", "content").to_s.strip
16
- usage = data["usage"] || {}
17
-
18
- build_response(
19
- content: content,
23
+ {
24
+ content: data.dig("choices", 0, "message", "content").to_s.strip,
20
25
  provider: :openai,
21
- model: data["model"] || request.model,
22
- usage: { prompt: usage["prompt_tokens"], completion: usage["completion_tokens"] },
23
- raw: raw
24
- )
26
+ model: data["model"] || model,
27
+ usage: extract_usage_openai(data)
28
+ }
25
29
  end
26
30
 
27
31
  private
28
32
 
29
- def build_body(request)
30
- body = {
31
- model: request.model,
32
- messages: request.messages,
33
- temperature: request.temperature
34
- }
35
- body[:response_format] = { type: "json_object" } if request.response_format == :json
36
- body.to_json
37
- end
38
-
39
- def post(body, timeout)
40
- ActiveHarness.config.http_client.post(
41
- API_URL,
42
- headers: {
43
- "Content-Type" => "application/json",
44
- "Authorization" => "Bearer #{api_key}"
45
- },
46
- body: body,
47
- timeout: timeout
48
- )
33
+ def api_key
34
+ key = ENV["OPENAI_API_KEY"].to_s
35
+ raise Errors::InvalidApiKeyError, "OPENAI_API_KEY is not set" if key.empty?
36
+ key
49
37
  end
50
38
 
51
39
  def handle_error!(data)
52
40
  return unless data["error"]
53
41
 
54
- message = data.dig("error", "message").to_s
55
- code = data.dig("error", "code").to_s
42
+ msg = data.dig("error", "message").to_s
43
+ code = data.dig("error", "code").to_s
44
+ type = data.dig("error", "type").to_s
45
+ metadata = data["error"].reject { |k, _| %w[message code type].include?(k) }
46
+ metadata = nil if metadata.empty?
56
47
 
57
48
  case code
58
49
  when "invalid_api_key", "unauthorized"
59
- raise Errors::InvalidApiKeyError, message
50
+ raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
60
51
  when "rate_limit_exceeded"
61
- raise Errors::RateLimitError, message
52
+ raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
62
53
  when "content_filter"
63
- raise Errors::SafetyBlockedError, message
54
+ raise Errors::SafetyBlockedError.new(msg, error_code: code, metadata: metadata)
55
+ when "500", "502", "503", "504"
56
+ raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
64
57
  else
65
- raise Errors::InvalidRequestError, message
58
+ case type
59
+ when "server_error"
60
+ raise Errors::ServerError.new(msg, error_code: code, metadata: metadata)
61
+ else
62
+ raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
63
+ end
66
64
  end
67
65
  end
68
-
69
- def api_key
70
- key = ActiveHarness.config.openai_api_key
71
- raise Errors::InvalidApiKeyError, "OPENAI_API_KEY not configured" if key.nil? || key.empty?
72
- key
73
- end
74
66
  end
75
67
  end
76
68
  end
@@ -1,80 +1,66 @@
1
- require "json"
1
+ require "uri"
2
2
 
3
3
  module ActiveHarness
4
4
  module Providers
5
- # OpenRouter proxies many models (OpenAI, Anthropic, etc.) through a single API.
6
- # API is OpenAI-compatible with an extra "HTTP-Referer" header.
5
+ # OpenRouter OpenAI-compatible API that proxies many models.
7
6
  class OpenRouter < Base
8
7
  API_URL = URI("https://openrouter.ai/api/v1/chat/completions")
9
8
 
10
- def call(request)
11
- body = build_body(request)
12
- raw = post(body, request.timeout)
13
- data = JSON.parse(raw)
9
+ # @param model [String]
10
+ # @param messages [Array<Hash>] [{role:, content:}, ...]
11
+ # @param temperature [Float]
12
+ # @param stream [Proc, nil] if given, called with each token as it arrives
13
+ # @return [Hash] { content:, provider:, model: }
14
+ def call(model:, messages:, temperature: 0.7, stream: nil)
15
+ headers = {
16
+ "Content-Type" => "application/json",
17
+ "Authorization" => "Bearer #{api_key}",
18
+ "HTTP-Referer" => "https://github.com/the-teacher/ActiveHarness"
19
+ }
20
+ body = { model: model, messages: messages, temperature: temperature }
14
21
 
15
- handle_error!(data)
22
+ if stream
23
+ body[:stream] = true
24
+ content = post_json_stream(API_URL, headers: headers, body: body, on_token: stream)
25
+ return { content: content, provider: :openrouter, model: model }
26
+ end
16
27
 
17
- content = data.dig("choices", 0, "message", "content").to_s.strip
18
- usage = data["usage"] || {}
28
+ raw = post_json(API_URL, headers: headers, body: body)
29
+ data = parse!(raw)
30
+ handle_error!(data)
19
31
 
20
- build_response(
21
- content: content,
32
+ {
33
+ content: data.dig("choices", 0, "message", "content").to_s.strip,
22
34
  provider: :openrouter,
23
- model: data["model"] || request.model,
24
- usage: { prompt: usage["prompt_tokens"], completion: usage["completion_tokens"] },
25
- raw: raw
26
- )
35
+ model: data["model"] || model,
36
+ usage: extract_usage_openai(data)
37
+ }
27
38
  end
28
39
 
29
40
  private
30
41
 
31
- def build_body(request)
32
- body = {
33
- model: request.model,
34
- messages: request.messages,
35
- temperature: request.temperature
36
- }
37
- body[:response_format] = { type: "json_object" } if request.response_format == :json
38
- body.to_json
39
- end
40
-
41
- def post(body, timeout)
42
- ActiveHarness.config.http_client.post(
43
- API_URL,
44
- headers: {
45
- "Content-Type" => "application/json",
46
- "Authorization" => "Bearer #{api_key}",
47
- "HTTP-Referer" => "https://github.com/the-teacher/ActiveHarness"
48
- },
49
- body: body,
50
- timeout: timeout
51
- )
42
+ def api_key
43
+ key = ENV["OPENROUTER_API_KEY"].to_s
44
+ raise Errors::InvalidApiKeyError, "OPENROUTER_API_KEY is not set" if key.empty?
45
+ key
52
46
  end
53
47
 
54
48
  def handle_error!(data)
55
49
  return unless data["error"]
56
50
 
57
- error = data["error"]
58
- message = error["message"].to_s
59
- code = error["code"].to_s
60
- meta = error.reject { |k, _| k == "message" }
61
- full = meta.empty? ? message : "#{message} | #{meta.inspect}"
51
+ msg = data.dig("error", "message").to_s
52
+ code = data.dig("error", "code").to_s
53
+ metadata = data.dig("error", "metadata")
62
54
 
63
55
  case code
64
- when "401"
65
- raise Errors::InvalidApiKeyError, full
66
- when "429"
67
- raise Errors::RateLimitError, full
68
- else
69
- raise Errors::InvalidRequestError, full
56
+ when "401" then raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
57
+ when "402" then raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
58
+ when "429" then raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
59
+ when "500", "502",
60
+ "503", "504" then raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
61
+ else raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
70
62
  end
71
63
  end
72
-
73
- def api_key
74
- key = ActiveHarness.config.openrouter_api_key
75
- raise Errors::InvalidApiKeyError, "OPENROUTER_API_KEY not configured" if key.nil? || key.empty?
76
- key
77
- end
78
64
  end
79
65
  end
80
66
  end
@@ -0,0 +1,12 @@
1
+ module ActiveHarness
2
+ class Railtie < Rails::Railtie
3
+ APP_AI_DIRS = %w[agents prompts tribunals pipelines memory].freeze
4
+
5
+ initializer "active_harness.autoload_paths" do |app|
6
+ APP_AI_DIRS.each do |dir|
7
+ path = Rails.root.join("app", "ai", dir)
8
+ app.config.autoload_paths << path.to_s if path.exist?
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ require "json"
2
+
3
+ module ActiveHarness
4
+ # Minimal result wrapper returned by Agent#call.
5
+ #
6
+ # output — raw string from the provider
7
+ # parsed — for format :json: a Ruby Hash/Array; for format :text: same as output
8
+ # usage — token counts: { input_tokens:, output_tokens:, total_tokens: } or nil for streaming
9
+ Result = Struct.new(:input, :output, :parsed, :system_prompt, :provider, :model, :temperature, :model_list, :attempts, :execution_time, :usage, keyword_init: true)
10
+ end