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
@@ -0,0 +1,216 @@
1
+ begin
2
+ require "concurrent"
3
+ rescue LoadError
4
+ raise LoadError,
5
+ "ActiveHarness::Tribunal requires the 'concurrent-ruby' gem. " \
6
+ "Add `gem 'concurrent-ruby'` to your Gemfile."
7
+ end
8
+
9
+ module ActiveHarness
10
+ # Can be used directly or subclassed with a class-level DSL.
11
+ #
12
+ # Direct usage:
13
+ # tribunal = ActiveHarness::Tribunal.new(
14
+ # input: "Is this message toxic?",
15
+ # context: { user_id: 42 },
16
+ # agents: [ToxicityAgent, BiasAgent, SpamAgent],
17
+ # timeout: 7
18
+ # )
19
+ # tribunal.on(:after_agent) { |result| puts result.model }
20
+ # tribunal.process { |results| results.all? { |r| r.parsed["result"] == true } }
21
+ # tribunal.call
22
+ #
23
+ # Subclass with DSL:
24
+ # class ContentQualityTribunal < ActiveHarness::Tribunal
25
+ # agents PolitenessAgent, ConstructivenessAgent
26
+ # on(:after_agent) { |result| puts result.model }
27
+ # process { |results| results.all? { |r| r.parsed["result"] == true } }
28
+ # end
29
+ # ContentQualityTribunal.new(input: "...").call
30
+ #
31
+ class Tribunal
32
+ VALID_HOOKS = %i[
33
+ before_call
34
+ after_agent
35
+ agent_error
36
+ after_call
37
+ before_verdict
38
+ after_verdict
39
+ ].freeze
40
+
41
+ # -------------------------------------------------------------------------
42
+ # Class-level DSL — used when subclassing ActiveHarness::Tribunal
43
+ # -------------------------------------------------------------------------
44
+ class << self
45
+ # Declare agents at the class level.
46
+ # agents PolitenessAgent, ConstructivenessAgent
47
+ def agents(*list)
48
+ tribunal_config[:agents] = list.flatten
49
+ end
50
+
51
+ # Class-level hook registration.
52
+ # on(:after_agent) { |result| puts result.model }
53
+ def on(event, &block)
54
+ unless VALID_HOOKS.include?(event)
55
+ raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
56
+ end
57
+
58
+ tribunal_config[:hooks][event] = block
59
+ end
60
+
61
+ # Rails-style aliases for +on+:
62
+ #
63
+ # before :call do ... end # → on :before_call
64
+ # before :agent do ... end # → on :before_agent (not used yet)
65
+ # before :verdict do |r| end # → on :before_verdict
66
+ # after :call do ... end # → on :after_call
67
+ # after :agent do |r| end # → on :after_agent
68
+ # after :verdict do |v| end # → on :after_verdict
69
+ # callback :agent_error do |n,e| end # → on :agent_error
70
+ def before(event, &block)
71
+ on(:"before_#{event}", &block)
72
+ end
73
+
74
+ def after(event, &block)
75
+ on(:"after_#{event}", &block)
76
+ end
77
+
78
+ def callback(event, &block)
79
+ on(event, &block)
80
+ end
81
+
82
+ # Class-level process block.
83
+ # process { |results| results.all? { |r| r.parsed["result"] == true } }
84
+ def process(&block)
85
+ tribunal_config[:process] = block
86
+ end
87
+
88
+ def tribunal_config
89
+ @tribunal_config ||= { agents: [], hooks: {} }
90
+ end
91
+
92
+ def inherited(subclass)
93
+ subclass.instance_variable_set(:@tribunal_config, { agents: [], hooks: {} })
94
+ end
95
+ end
96
+
97
+ attr_accessor :input
98
+ attr_reader :results, :errors, :verdict, :execution_time, :agent_execution_times
99
+
100
+ def initialize(input: nil, context: {}, agents: nil, timeout: 7)
101
+ config = self.class.tribunal_config
102
+
103
+ @input = input
104
+ @context = context
105
+ @agents = agents || config[:agents]
106
+ @timeout = timeout
107
+ @process_block = config[:process]
108
+ @hooks = config[:hooks].dup
109
+ @results = []
110
+ @errors = []
111
+ @verdict = nil
112
+ @execution_time = nil
113
+ @agent_execution_times = []
114
+ end
115
+
116
+ # Instance-level hook registration — overrides class-level hooks.
117
+ # :before_verdict is a transform hook: its return value replaces the results array.
118
+ def on(event, &block)
119
+ unless VALID_HOOKS.include?(event)
120
+ raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
121
+ end
122
+
123
+ @hooks[event] = block
124
+ self
125
+ end
126
+
127
+ # Instance-level process block — overrides class-level block.
128
+ def process(&block)
129
+ @process_block = block
130
+ self
131
+ end
132
+
133
+ # Run all agents in parallel, then compute the verdict.
134
+ # Returns self so calls can be chained: tribunal.call.verdict
135
+ #
136
+ # Behaviour on failure:
137
+ # - If some agents fail/timeout, their errors are in #errors and
138
+ # #results contains only successful results.
139
+ # - If ALL agents fail/timeout, raises Errors::AllAgentsFailed.
140
+ def call
141
+ agents = resolve_agents
142
+ run_hook(:before_call)
143
+
144
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
145
+
146
+ futures = agents.map do |agent|
147
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
148
+ future = Concurrent::Future.execute { agent.call }
149
+ [future, t0]
150
+ end
151
+
152
+ @results = []
153
+ @errors = []
154
+ @agent_execution_times = []
155
+
156
+ futures.each_with_index do |(future, t0), index|
157
+ future.wait(@timeout)
158
+ elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
159
+ @agent_execution_times << { agent: agents[index].class.name, time: elapsed }
160
+
161
+ if future.fulfilled?
162
+ @results << future.value
163
+ run_hook(:after_agent, future.value)
164
+ elsif future.incomplete?
165
+ error = Errors::TimeoutError.new(
166
+ "Agent #{agents[index].class.name} timed out after #{@timeout}s"
167
+ )
168
+ @errors << { agent: agents[index].class.name, error: error }
169
+ run_hook(:agent_error, agents[index].class.name, error)
170
+ else
171
+ @errors << { agent: agents[index].class.name, error: future.reason }
172
+ run_hook(:agent_error, agents[index].class.name, future.reason)
173
+ end
174
+ end
175
+
176
+ @execution_time = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at).round(3)
177
+
178
+ run_hook(:after_call, @results, @errors)
179
+
180
+ if @results.empty?
181
+ messages = @errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
182
+ raise Errors::AllAgentsFailed, "All agents failed — #{messages}"
183
+ end
184
+
185
+ verdict_input = transform_hook(:before_verdict, @results)
186
+ @verdict = @process_block ? @process_block.call(verdict_input) : nil
187
+ run_hook(:after_verdict, @verdict)
188
+
189
+ self
190
+ end
191
+
192
+ private
193
+
194
+ def run_hook(event, *args)
195
+ @hooks[event]&.call(*args)
196
+ end
197
+
198
+ # Like run_hook but uses the return value to replace the passed value.
199
+ def transform_hook(event, value)
200
+ return value unless @hooks[event]
201
+
202
+ @hooks[event].call(value)
203
+ end
204
+
205
+ def resolve_agents
206
+ @agents.map do |agent|
207
+ if agent.is_a?(Class)
208
+ agent.new(input: @input, context: @context.dup)
209
+ else
210
+ agent.input = @input if @input
211
+ agent
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -1,49 +1,20 @@
1
- require "active_harness/core/version"
2
- require "active_harness/core/errors"
3
- require "active_harness/core/configuration"
4
- require "active_harness/payload"
5
-
6
- require "active_harness/http/client"
7
- require "active_harness/http/retry_policy"
8
-
9
- require "active_harness/rate_limit/request_limiter"
10
- require "active_harness/rate_limit/risk_holdback"
11
-
12
- require "active_harness/models/model_request"
13
- require "active_harness/models/model_response"
14
- require "active_harness/results/input_result"
15
- require "active_harness/results/debug_result"
16
- require "active_harness/results/result"
17
-
18
- require "active_harness/providers/base"
19
- require "active_harness/providers/openai"
20
- require "active_harness/providers/openrouter"
21
- require "active_harness/providers/anthropic"
22
- require "active_harness/providers/google"
23
-
24
- require "active_harness/prompts/guard_system_prompt"
25
-
26
- require "active_harness/pipeline/provider_registry"
27
- require "active_harness/pipeline/prompt_builder"
28
- require "active_harness/pipeline/output_parser"
29
- require "active_harness/pipeline/fallback_runner"
30
- require "active_harness/pipeline/guard_runner"
31
- require "active_harness/pipeline/engine"
32
- require "active_harness/agent"
1
+ require_relative "active_harness/core/errors"
2
+ require_relative "active_harness/result"
3
+ require_relative "active_harness/http/client"
4
+ require_relative "active_harness/http/streaming_client"
5
+ require_relative "active_harness/providers/base"
6
+ require_relative "active_harness/providers/openai"
7
+ require_relative "active_harness/providers/openrouter"
8
+ require_relative "active_harness/providers/groq"
9
+ require_relative "active_harness/providers/gemini"
10
+ require_relative "active_harness/providers/anthropic"
11
+ require_relative "active_harness/memory"
12
+ require_relative "active_harness/agent"
13
+ require_relative "active_harness/tribunal"
14
+ require_relative "active_harness/pipeline"
15
+
16
+ require_relative "active_harness/railtie" if defined?(Rails::Railtie)
33
17
 
34
18
  module ActiveHarness
35
- class << self
36
- def configure
37
- yield configuration
38
- end
39
-
40
- def configuration
41
- @configuration ||= Configuration.new
42
- end
43
- alias config configuration
44
-
45
- def reset!
46
- @configuration = nil
47
- end
48
- end
19
+ VERSION = "0.2.0"
49
20
  end
@@ -0,0 +1,16 @@
1
+ require "rails/generators"
2
+
3
+ module ActiveHarness
4
+ module Generators
5
+ class AgentGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates an ActiveHarness agent in app/ai/agents/"
9
+
10
+ def create_agent
11
+ template "agent.rb.tt",
12
+ "app/ai/agents/#{file_name}_agent.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ class <%= class_name %>Agent < ActiveHarness::Agent
2
+ system_prompt <%= class_name %>Prompt
3
+
4
+ model do
5
+ use provider: :openrouter, model: "mistralai/mistral-nemo"
6
+ fallback provider: :openrouter, model: "meta-llama/llama-3.1-8b-instruct"
7
+ end
8
+ end
@@ -0,0 +1,54 @@
1
+ require "rails/generators"
2
+
3
+ module ActiveHarness
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc "Creates the app/ai/ directory structure for ActiveHarness"
7
+
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ APP_AI_DIRS = %w[agents prompts tribunals pipelines memory].freeze
11
+
12
+ def create_structure
13
+ APP_AI_DIRS.each do |dir|
14
+ empty_directory "app/ai/#{dir}"
15
+ copy_templates_if_empty(dir)
16
+ end
17
+ end
18
+
19
+ def copy_controller
20
+ target = File.join(destination_root, "app", "controllers", "ai_test_support_controller.rb")
21
+ return if File.exist?(target)
22
+
23
+ copy_file "controllers/ai_controller.rb",
24
+ "app/controllers/ai_test_support_controller.rb"
25
+ end
26
+
27
+ def inject_routes
28
+ route <<~ROUTES.strip
29
+ # ActiveHarness — AI test support endpoints
30
+ post "ai/agent", to: "ai_test_support#agent"
31
+ post "ai/agent_memory", to: "ai_test_support#agent_memory"
32
+ post "ai/tribunal", to: "ai_test_support#tribunal"
33
+ post "ai/pipeline", to: "ai_test_support#pipeline"
34
+ get "ai/agent_stream", to: "ai_test_support#agent_stream"
35
+ ROUTES
36
+ end
37
+
38
+ private
39
+
40
+ def copy_templates_if_empty(dir)
41
+ target_dir = File.join(destination_root, "app", "ai", dir)
42
+ return if Dir.glob(File.join(target_dir, "*.rb")).any?
43
+
44
+ template_dir = File.join(self.class.source_root, dir)
45
+ return unless Dir.exist?(template_dir)
46
+
47
+ Dir[File.join(template_dir, "*.rb")].sort.each do |tpl|
48
+ copy_file File.join(dir, File.basename(tpl)),
49
+ File.join("app", "ai", dir, File.basename(tpl))
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "../prompts/test_support_prompt"
2
+
3
+ class TestSupportAgent < ActiveHarness::Agent
4
+ system_prompt TestSupportPrompt
5
+
6
+ model do
7
+ use provider: :openrouter, model: "mistralai/mistral-nemo"
8
+ fallback provider: :openrouter, model: "meta-llama/llama-3.1-8b-instruct"
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "../prompts/test_support_guard_prompt"
2
+
3
+ class TestSupportGuardAgent < ActiveHarness::Agent
4
+ system_prompt TestSupportGuardPrompt
5
+ format :json
6
+
7
+ model do
8
+ use provider: :openrouter, model: "meta-llama/llama-3.1-8b-instruct"
9
+ fallback provider: :openrouter, model: "mistralai/mistral-nemo"
10
+ end
11
+ end
@@ -0,0 +1,105 @@
1
+ class AiTestSupportController < ApplicationController
2
+ include ActionController::Live
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # POST /ai/agent
6
+ # body: { input: "What is your return policy?" }
7
+ # ---------------------------------------------------------------------------
8
+ def agent
9
+ result = TestSupportAgent.call(input: params.require(:input))
10
+
11
+ render json: {
12
+ output: result.output,
13
+ model: result.model,
14
+ time: result.execution_time
15
+ }
16
+ end
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # POST /ai/agent_memory
20
+ # body: { input: "Does that apply to accessories?", session_id: "user_42" }
21
+ #
22
+ # Uses AppMemory so the same session keeps conversational context
23
+ # across multiple requests.
24
+ # ---------------------------------------------------------------------------
25
+ def agent_memory
26
+ memory = TestSupportMemory.new(session_id: params.require(:session_id))
27
+ result = TestSupportAgent.call(input: params.require(:input), memory: memory)
28
+
29
+ render json: {
30
+ output: result.output,
31
+ model: result.model,
32
+ time: result.execution_time,
33
+ turns: memory.size
34
+ }
35
+ end
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # POST /ai/tribunal
39
+ # body: { input: "Buy cheap pills now!!!" }
40
+ #
41
+ # Returns verdict: true (safe) or false (rejected).
42
+ # ---------------------------------------------------------------------------
43
+ def tribunal
44
+ result = TestSupportGuardTribunal.call(input: params.require(:input))
45
+
46
+ render json: {
47
+ verdict: result.verdict,
48
+ time: result.execution_time
49
+ }
50
+ end
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # POST /ai/pipeline
54
+ # body: { input: "What is your return policy?" }
55
+ #
56
+ # Runs the full TestSupportPipeline.
57
+ # If a guard step stops the pipeline early, stopped: true is returned.
58
+ # ---------------------------------------------------------------------------
59
+ def pipeline
60
+ pipe = TestSupportPipeline.new(input: params.require(:input))
61
+ pipe.call
62
+
63
+ if pipe.stopped?
64
+ render json: { stopped: true, stopped_at: pipe.stopped_at }
65
+ else
66
+ render json: { stopped: false, output: pipe.output }
67
+ end
68
+ end
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # GET /ai/agent_stream?input=What+is+your+return+policy%3F
72
+ #
73
+ # Streams the response token by token using Server-Sent Events.
74
+ # Each token arrives as: data: {"token":"..."}
75
+ # End of stream is marked: data: {"done":true}
76
+ #
77
+ # JavaScript client example:
78
+ # const es = new EventSource('/ai/agent_stream?input=Hello');
79
+ # es.onmessage = ({ data }) => {
80
+ # const { token, done } = JSON.parse(data);
81
+ # if (done) { es.close(); return; }
82
+ # document.querySelector('#output').insertAdjacentText('beforeend', token);
83
+ # };
84
+ # ---------------------------------------------------------------------------
85
+ def agent_stream
86
+ input = params.require(:input)
87
+
88
+ response.headers["Content-Type"] = "text/event-stream"
89
+ response.headers["Cache-Control"] = "no-cache"
90
+ response.headers["X-Accel-Buffering"] = "no" # disable nginx buffering
91
+
92
+ sse = ActionController::Live::SSE.new(response.stream, event: "message")
93
+
94
+ TestSupportAgent.call(
95
+ input: input,
96
+ stream: ->(token) { sse.write({ token: token }.to_json) }
97
+ )
98
+
99
+ sse.write({ done: true }.to_json)
100
+ rescue ActionController::Live::ClientDisconnected
101
+ # client closed the connection — normal, nothing to do
102
+ ensure
103
+ sse.close
104
+ end
105
+ end
@@ -0,0 +1,16 @@
1
+ class TestSupportMemory < ActiveHarness::Memory
2
+ # Usage: TestSupportMemory.new(session_id: "user_42")
3
+ #
4
+ # Wraps ActiveHarness::Memory with project defaults so callers
5
+ # only need to pass a session_id.
6
+ def initialize(session_id:)
7
+ super(
8
+ session_id: session_id,
9
+ depth: 10,
10
+ adapter: :file,
11
+ path: Rails.root.join("storage", "ai", "memory").to_s,
12
+ storage_size: 200,
13
+ pretty: Rails.env.development?
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "../tribunals/test_support_guard_tribunal"
2
+ require_relative "../agents/test_support_agent"
3
+
4
+ # Two-step pipeline: spam guard → answer.
5
+ # Add more steps between them as needed.
6
+ class TestSupportPipeline < ActiveHarness::Pipeline
7
+ # Step 1 — GUARD: reject spam before spending tokens on an answer
8
+ step :spam_guard do
9
+ use TestSupportGuardTribunal
10
+ stop_if ->(result) { result.verdict == false }
11
+ end
12
+
13
+ # Step 2 — RESPOND: generate the actual answer
14
+ step :respond, TestSupportAgent
15
+
16
+ before :step do |step_name, _payload|
17
+ puts "[pipeline] → :#{step_name}"
18
+ end
19
+
20
+ after :step do |step_name, _result|
21
+ puts "[pipeline] ✓ :#{step_name}"
22
+ end
23
+
24
+ callback :stopped do |step_name, _result|
25
+ puts "[pipeline] ✗ STOPPED at :#{step_name}"
26
+ end
27
+
28
+ callback :complete do |_last_result|
29
+ puts "[pipeline] ✓ complete"
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ class TestSupportGuardPrompt
2
+ def call
3
+ <<~PROMPT.strip
4
+ You are a spam detection filter.
5
+ Analyze the message and reply ONLY with valid JSON, no markdown:
6
+ {"spam": true, "reason": "..."} or {"spam": false, "reason": "..."}
7
+ PROMPT
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class TestSupportPrompt
2
+ def call
3
+ "You are a concise and helpful assistant. Answer in 1-2 sentences."
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "../agents/test_support_guard_agent"
2
+
3
+ # Runs TestSupportGuardAgent in parallel (single agent here, extendable).
4
+ # Verdict is true (safe) when no spam is detected.
5
+ class TestSupportGuardTribunal < ActiveHarness::Tribunal
6
+ agents TestSupportGuardAgent
7
+
8
+ process do |results|
9
+ results.none? { |r| r.parsed["spam"] == true }
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ require "rails/generators"
2
+
3
+ module ActiveHarness
4
+ module Generators
5
+ class MemoryGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates an ActiveHarness memory class in app/ai/memory/"
9
+
10
+ def create_memory
11
+ template "memory.rb.tt",
12
+ "app/ai/memory/#{file_name}_memory.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ class <%= class_name %>Memory < ActiveHarness::Memory
2
+ def initialize(session_id:)
3
+ super(
4
+ session_id: session_id,
5
+ depth: 10,
6
+ adapter: :file,
7
+ path: Rails.root.join("storage", "ai", "memory").to_s,
8
+ storage_size: 200,
9
+ pretty: Rails.env.development?
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ require "rails/generators"
2
+
3
+ module ActiveHarness
4
+ module Generators
5
+ class PipelineGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates an ActiveHarness pipeline in app/ai/pipelines/"
9
+
10
+ def create_pipeline
11
+ template "pipeline.rb.tt",
12
+ "app/ai/pipelines/#{file_name}_pipeline.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ class <%= class_name %>Pipeline < ActiveHarness::Pipeline
2
+ step :respond, <%= class_name %>Agent
3
+
4
+ before :step do |step_name, _payload|
5
+ puts "[pipeline] → :#{step_name}"
6
+ end
7
+
8
+ after :step do |step_name, _result|
9
+ puts "[pipeline] ✓ :#{step_name}"
10
+ end
11
+
12
+ callback :stopped do |step_name, _result|
13
+ puts "[pipeline] ✗ STOPPED at :#{step_name}"
14
+ end
15
+
16
+ callback :complete do |_last_result|
17
+ puts "[pipeline] ✓ complete"
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ require "rails/generators"
2
+
3
+ module ActiveHarness
4
+ module Generators
5
+ class PromptGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates an ActiveHarness prompt in app/ai/prompts/"
9
+
10
+ def create_prompt
11
+ template "prompt.rb.tt",
12
+ "app/ai/prompts/#{file_name}_prompt.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ class <%= class_name %>Prompt
2
+ def call
3
+ "You are a concise and helpful assistant. Answer in 1-2 sentences."
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>Tribunal < ActiveHarness::Tribunal
2
+ agents <%= class_name %>Agent
3
+
4
+ process do |results|
5
+ results.all? { |r| r.parsed["result"] == true }
6
+ end
7
+ end