active_harness 0.2.20 → 0.2.22

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1188ca468f31f46c40d118b7e719a849925aa11ddab3a4a87ea4df0fd9bdb84
4
- data.tar.gz: 1ed36314efd943e6f4e62bbc43b2ec00d0b0486bdbf015e6b590341d240ffcdc
3
+ metadata.gz: 33e2cd8c861873028568322fd36104414d698fb4888810388d0ac9b1c1b5b9d4
4
+ data.tar.gz: cd792deb836b9cbb5237f9c8c02a6f436c3c48162b21edb412f9797a39a94e16
5
5
  SHA512:
6
- metadata.gz: 37e82033bddd4069f26619d2bfc5faec831c32618216bedb6cb9ce19ca7fbbaac1057e829809ee15f1d88d1a6fc3b3832cd2c02d1f112c850a974a26205d3727
7
- data.tar.gz: 3a4fafd8184d55f116689e28c4f411d102f7279d290093c9cc32dafabfc9eafe9aa55abde59a04cec37985e74da94a8ddb14153febce35aa41c2eb0404eed2c6
6
+ metadata.gz: 93365a99d1965c177e379a48027f5fc2dee3fa06dba5061d41be5b7948ba134ed8307cf65c4e57951f21798150c05745311ee793c49fb08be11669ebb13f8e8d
7
+ data.tar.gz: 3fb4814a98bde51eba1982f55cd22457bd96613be7f1b44b08434fe0c3c3dc467d7d6ccb0fe36cbbb590885139c1b047c9dc79b5293e7d5fd4f5be32df4e5ca7
@@ -32,7 +32,7 @@ module ActiveHarness
32
32
  end
33
33
 
34
34
  agent_config[:hooks] ||= {}
35
- agent_config[:hooks][event] = block
35
+ (agent_config[:hooks][event] ||= []) << block
36
36
  end
37
37
 
38
38
  # Rails-style aliases for +on+:
@@ -59,27 +59,20 @@ module ActiveHarness
59
59
  end
60
60
  end
61
61
 
62
+ include Core::HookRunner
63
+
62
64
  private
63
65
 
64
66
  def run_hook(event, *args)
65
- hooks = @config[:hooks] || {}
66
- return unless hooks[event]
67
-
68
- if args.any?
69
- instance_exec(*args, &hooks[event])
70
- else
71
- instance_eval(&hooks[event])
72
- end
67
+ run_hooks(@config[:hooks] || {}, event, *args)
73
68
  end
74
69
 
75
70
  # Unified internal method: fires the DSL hook AND the external event_stream lambda.
76
71
  # Consistent with Tribunal#fire and Pipeline#fire.
77
72
  def fire(event, *args)
78
- result = run_hook(event, *args)
73
+ run_hook(event, *args)
79
74
  @event_stream&.call(event, *args)
80
- result
81
75
  rescue IOError, ActionController::Live::ClientDisconnected
82
- result
83
76
  end
84
77
  end
85
78
  end
@@ -11,8 +11,22 @@ module ActiveHarness
11
11
  # SupportAgent.call(input: "Hi")
12
12
  # SupportAgent.call(input: "Hi", context: { user_id: 42 })
13
13
  # SupportAgent.call(input: "Hi", memory: memory)
14
- def call(input: nil, context: {}, models: nil, memory: nil, streams: {})
15
- new(input: input, context: context, models: models, memory: memory, streams: streams).call
14
+ def call(
15
+ input: nil,
16
+ context: {},
17
+ params: {},
18
+ models: nil,
19
+ memory: nil,
20
+ streams: {}
21
+ )
22
+ new(
23
+ input: input,
24
+ context: context,
25
+ params: params,
26
+ models: models,
27
+ memory: memory,
28
+ streams: streams
29
+ ).call
16
30
  end
17
31
 
18
32
  # Each subclass gets its own isolated config hash.
@@ -36,8 +50,12 @@ module ActiveHarness
36
50
  # -------------------------------------------------------------------------
37
51
  # Instance API
38
52
  # -------------------------------------------------------------------------
39
- attr_accessor :input, :context
40
- attr_reader :result, :token_stream, :event_stream
53
+ attr_accessor :input,
54
+ :context,
55
+ :params
56
+ attr_reader :result,
57
+ :token_stream,
58
+ :event_stream
41
59
 
42
60
  def models=(list)
43
61
  @models_override = Array(list)
@@ -48,11 +66,19 @@ module ActiveHarness
48
66
  @memory = obj
49
67
  end
50
68
 
51
- def initialize(input: nil, context: {}, models: nil, memory: nil, streams: {})
69
+ def initialize(
70
+ input: nil,
71
+ context: {},
72
+ params: {},
73
+ models: nil,
74
+ memory: nil,
75
+ streams: {}
76
+ )
52
77
  @input = input
53
78
  @config = self.class.agent_config
54
79
  normalize_input!
55
80
  @context = context
81
+ @params = params
56
82
  @models_override = Array(models) if models
57
83
  @token_stream = streams[:token]
58
84
  @event_stream = streams[:agent]
@@ -0,0 +1,26 @@
1
+ module ActiveHarness
2
+ module Core
3
+ # Shared hook execution logic included by Agent, Tribunal, and Pipeline.
4
+ #
5
+ # Hooks are stored in arrays so multiple +on+/+before+/+after+/+callback+
6
+ # calls with the same event name accumulate — later registrations append
7
+ # rather than overwrite. This lets modules register default hooks without
8
+ # blocking user-defined hooks on the same event.
9
+ #
10
+ # class MyAgent < ActiveHarness::Agent
11
+ # include SomeTracingConcern # registers before(:call) internally
12
+ # before(:call) { ... } # appends — both hooks run in order
13
+ # end
14
+ module HookRunner
15
+ private
16
+
17
+ # Execute every block registered for +event+, passing +args+ to each.
18
+ # Blocks run in the receiver's instance context (instance_exec / instance_eval).
19
+ def run_hooks(hooks_hash, event, *args)
20
+ Array(hooks_hash[event]).each do |blk|
21
+ args.any? ? instance_exec(*args, &blk) : instance_eval(&blk)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,77 @@
1
+ module ActiveHarness
2
+ class Pipeline
3
+ VALID_HOOKS = %i[before_step after_step stopped complete].freeze
4
+ VALID_STEP_HOOKS = %i[before_step after_step].freeze
5
+
6
+ class << self
7
+ # Register a global or per-step hook.
8
+ #
9
+ # Global hooks fire on every step:
10
+ # on :before_step do |step_name, payload| ... end
11
+ # on :after_step do |step_name, result| ... end
12
+ # on :stopped do |step_name, result| ... end
13
+ # on :complete do |last_result| ... end
14
+ #
15
+ # Per-step hooks fire only for the named step (no step_name passed):
16
+ # on :before_step, :translate do |payload| ... end
17
+ # on :after_step, :translate do |result| ... end
18
+ def on(event, step_name = nil, &block)
19
+ if step_name
20
+ unless VALID_STEP_HOOKS.include?(event)
21
+ raise ArgumentError,
22
+ "Per-step hooks support: #{VALID_STEP_HOOKS.join(", ")}. Got :#{event}"
23
+ end
24
+ pipeline_config[:step_hooks][step_name] ||= {}
25
+ (pipeline_config[:step_hooks][step_name][event] ||= []) << block
26
+ else
27
+ unless VALID_HOOKS.include?(event)
28
+ raise ArgumentError,
29
+ "Unknown Pipeline hook :#{event}. Valid: #{VALID_HOOKS.join(", ")}"
30
+ end
31
+ (pipeline_config[:hooks][event] ||= []) << block
32
+ end
33
+ end
34
+
35
+ # Rails-style aliases for +on+:
36
+ #
37
+ # Global:
38
+ # before :step do |name, payload| end # → on :before_step
39
+ # after :step do |name, result| end # → on :after_step
40
+ # callback :stopped do |name, result| end # → on :stopped
41
+ # callback :complete do |result| end # → on :complete
42
+ #
43
+ # Per-step:
44
+ # after :step, :translate do |result| end
45
+ # before :step, :translate do |payload| end
46
+ def before(event, step_name = nil, &block)
47
+ on(:"before_#{event}", step_name, &block)
48
+ end
49
+
50
+ def after(event, step_name = nil, &block)
51
+ on(:"after_#{event}", step_name, &block)
52
+ end
53
+
54
+ def callback(event, &block)
55
+ on(event, &block)
56
+ end
57
+ end
58
+
59
+ include Core::HookRunner
60
+
61
+ private
62
+
63
+ # Fires global hook AND pipeline_event_stream. Consistent with Agent#fire and Tribunal#fire.
64
+ def fire(event, step_name, data, config)
65
+ run_hooks(config[:hooks], event, step_name, data)
66
+ @pipeline_event_stream&.call(event, step_name, data)
67
+ rescue IOError, ActionController::Live::ClientDisconnected
68
+ nil
69
+ end
70
+
71
+ # Per-step hook: receives (data) only — not forwarded to pipeline_event_stream
72
+ # (global fire already covers the step event with step_name context).
73
+ def fire_step(event, step_name, data, config)
74
+ run_hooks(config[:step_hooks][step_name] || {}, event, data)
75
+ end
76
+ end
77
+ end
@@ -32,9 +32,6 @@ module ActiveHarness
32
32
  # pipeline.step_results # => { translate: <Result>, ... }
33
33
  #
34
34
  class Pipeline
35
- VALID_HOOKS = %i[before_step after_step stopped complete].freeze
36
- VALID_STEP_HOOKS = %i[before_step after_step].freeze
37
-
38
35
  # -------------------------------------------------------------------------
39
36
  # Class-level DSL
40
37
  # -------------------------------------------------------------------------
@@ -53,57 +50,6 @@ module ActiveHarness
53
50
  pipeline_config[:steps] << Pipeline::Step.new(name, agent_class, &block)
54
51
  end
55
52
 
56
- # Register a global or per-step hook.
57
- #
58
- # Global hooks fire on every step:
59
- # on :before_step do |step_name, payload| ... end
60
- # on :after_step do |step_name, result| ... end
61
- # on :stopped do |step_name, result| ... end
62
- # on :complete do |last_result| ... end
63
- #
64
- # Per-step hooks fire only for the named step (no step_name passed):
65
- # on :before_step, :translate do |payload| ... end
66
- # on :after_step, :translate do |result| ... end
67
- def on(event, step_name = nil, &block)
68
- if step_name
69
- unless VALID_STEP_HOOKS.include?(event)
70
- raise ArgumentError,
71
- "Per-step hooks support: #{VALID_STEP_HOOKS.join(", ")}. Got :#{event}"
72
- end
73
- pipeline_config[:step_hooks][step_name] ||= {}
74
- pipeline_config[:step_hooks][step_name][event] = block
75
- else
76
- unless VALID_HOOKS.include?(event)
77
- raise ArgumentError,
78
- "Unknown Pipeline hook :#{event}. Valid: #{VALID_HOOKS.join(", ")}"
79
- end
80
- pipeline_config[:hooks][event] = block
81
- end
82
- end
83
-
84
- # Rails-style aliases for +on+:
85
- #
86
- # Global:
87
- # before :step do |name, payload| end # → on :before_step
88
- # after :step do |name, result| end # → on :after_step
89
- # callback :stopped do |name, result| end # → on :stopped
90
- # callback :complete do |result| end # → on :complete
91
- #
92
- # Per-step:
93
- # after :step, :translate do |result| end
94
- # before :step, :translate do |payload| end
95
- def before(event, step_name = nil, &block)
96
- on(:"before_#{event}", step_name, &block)
97
- end
98
-
99
- def after(event, step_name = nil, &block)
100
- on(:"after_#{event}", step_name, &block)
101
- end
102
-
103
- def callback(event, &block)
104
- on(event, &block)
105
- end
106
-
107
53
  def pipeline_config
108
54
  @pipeline_config ||= { steps: [], hooks: {}, step_hooks: {} }
109
55
  end
@@ -120,19 +66,32 @@ module ActiveHarness
120
66
  # -------------------------------------------------------------------------
121
67
  # Instance API
122
68
  # -------------------------------------------------------------------------
123
- attr_reader :original_input, :output, :stopped_at, :stop_reason,
124
- :execution_time, :step_results, :context
125
- attr_writer :context
69
+ attr_reader :original_input,
70
+ :output,
71
+ :stopped_at,
72
+ :stop_reason,
73
+ :execution_time,
74
+ :step_results,
75
+ :context
76
+ attr_writer :context
77
+ attr_accessor :params
126
78
 
127
79
  def input=(value)
128
80
  @original_input = value
129
81
  @payload = value
130
82
  end
131
83
 
132
- def initialize(input:, context: {}, memory: nil, streams: {})
84
+ def initialize(
85
+ input:,
86
+ context: {},
87
+ params: {},
88
+ memory: nil,
89
+ streams: {}
90
+ )
133
91
  @original_input = input
134
92
  @payload = input
135
93
  @context = context.dup
94
+ @params = params
136
95
  @memory = memory
137
96
  @token_stream = streams[:token]
138
97
  @agent_event_stream = streams[:agent]
@@ -174,8 +133,7 @@ module ActiveHarness
174
133
  @stopped = true
175
134
  @stopped_at = step.name
176
135
  @stop_reason = result
177
- blk = config[:hooks][:stopped]
178
- instance_exec(step.name, result, &blk) if blk
136
+ run_hooks(config[:hooks], :stopped, step.name, result)
179
137
  @pipeline_event_stream&.call(:stopped, step.name, result)
180
138
  break
181
139
  end
@@ -192,8 +150,7 @@ module ActiveHarness
192
150
  )
193
151
 
194
152
  last_result = @step_results[@step_results.keys.last]
195
- blk = config[:hooks][:complete]
196
- instance_exec(last_result, &blk) if blk
153
+ run_hooks(config[:hooks], :complete, last_result)
197
154
  @pipeline_event_stream&.call(:complete, last_result)
198
155
  end
199
156
 
@@ -208,6 +165,7 @@ module ActiveHarness
208
165
  step.agent_class.new(
209
166
  input: @payload,
210
167
  context: @context.dup,
168
+ params: @params,
211
169
  streams: agent_streams
212
170
  ).call
213
171
  else
@@ -215,27 +173,13 @@ module ActiveHarness
215
173
  step.agent_class.new(
216
174
  input: @payload,
217
175
  context: @context.dup,
176
+ params: @params,
218
177
  streams: agent_streams
219
178
  ).call.result
220
179
  end
221
180
  end
222
-
223
- # Fires global hook AND pipeline_event_stream. Consistent with Agent#fire and Tribunal#fire.
224
- def fire(event, step_name, data, config)
225
- blk = config[:hooks][event]
226
- instance_exec(step_name, data, &blk) if blk
227
- @pipeline_event_stream&.call(event, step_name, data)
228
- rescue IOError, ActionController::Live::ClientDisconnected
229
- nil
230
- end
231
-
232
- # Per-step hook: receives (data) only — not forwarded to pipeline_event_stream
233
- # (global fire already covers the step event with step_name context).
234
- def fire_step(event, step_name, data, config)
235
- blk = config[:step_hooks][step_name]&.dig(event)
236
- instance_exec(data, &blk) if blk
237
- end
238
181
  end
239
182
  end
240
183
 
184
+ require_relative "pipeline/hooks"
241
185
  require_relative "pipeline/step"
@@ -26,7 +26,7 @@ module ActiveHarness
26
26
  "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
27
27
  end
28
28
 
29
- tribunal_config[:hooks][event] = block
29
+ (tribunal_config[:hooks][event] ||= []) << block
30
30
  end
31
31
 
32
32
  # Rails-style aliases for +on+:
@@ -51,7 +51,7 @@ module ActiveHarness
51
51
  end
52
52
  end
53
53
 
54
- # Instance-level hook registration — overrides class-level hooks for this instance.
54
+ # Instance-level hook registration — appends to class-level hooks for this event.
55
55
  # :before_verdict is a transform hook: its return value replaces the results array
56
56
  # passed to the process block.
57
57
  def on(event, &block)
@@ -60,20 +60,16 @@ module ActiveHarness
60
60
  "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
61
61
  end
62
62
 
63
- @hooks[event] = block
63
+ (@hooks[event] ||= []) << block
64
64
  self
65
65
  end
66
66
 
67
+ include Core::HookRunner
68
+
67
69
  private
68
70
 
69
71
  def run_hook(event, *args)
70
- return unless @hooks[event]
71
-
72
- if args.any?
73
- instance_exec(*args, &@hooks[event])
74
- else
75
- instance_eval(&@hooks[event])
76
- end
72
+ run_hooks(@hooks, event, *args)
77
73
  end
78
74
 
79
75
  # Fire the DSL-registered hook AND the external tribunal_event_stream lambda (if set).
@@ -84,12 +80,10 @@ module ActiveHarness
84
80
  nil
85
81
  end
86
82
 
87
- # Like run_hook but uses the return value to replace the passed value.
83
+ # Like run_hook but chains all blocks, passing each return value to the next.
88
84
  # Used by :before_verdict to allow results transformation before verdict computation.
89
85
  def transform_hook(event, value)
90
- return value unless @hooks[event]
91
-
92
- instance_exec(value, &@hooks[event])
86
+ Array(@hooks[event]).reduce(value) { |val, blk| instance_exec(val, &blk) }
93
87
  end
94
88
  end
95
89
  end
@@ -46,24 +46,39 @@ module ActiveHarness
46
46
  # -------------------------------------------------------------------------
47
47
  # Instance API
48
48
  # -------------------------------------------------------------------------
49
- attr_accessor :input, :context
50
- attr_reader :results, :errors, :verdict, :execution_time, :agent_execution_times,
51
- :token_stream, :agent_event_stream, :tribunal_event_stream
52
-
53
- def initialize(input: nil, context: {}, agents: nil, timeout: 7,
54
- streams: {},
55
- may_fail: :_unset)
49
+ attr_accessor :input,
50
+ :context,
51
+ :params
52
+ attr_reader :results,
53
+ :errors,
54
+ :verdict,
55
+ :execution_time,
56
+ :agent_execution_times,
57
+ :token_stream,
58
+ :agent_event_stream,
59
+ :tribunal_event_stream
60
+
61
+ def initialize(
62
+ input: nil,
63
+ context: {},
64
+ params: {},
65
+ agents: nil,
66
+ timeout: 7,
67
+ streams: {},
68
+ may_fail: :_unset
69
+ )
56
70
  config = self.class.tribunal_config
57
71
 
58
72
  @input = input
59
73
  @context = context
74
+ @params = params
60
75
  @agents = agents || config[:agents]
61
76
  @timeout = timeout
62
77
  @process_block = config[:process]
63
78
  @strategy = config[:strategy]
64
79
  @evaluate_block = config[:evaluate_block]
65
80
  @may_fail = may_fail == :_unset ? config[:may_fail] : may_fail
66
- @hooks = config[:hooks].dup
81
+ @hooks = config[:hooks].transform_values { |v| Array(v).dup }
67
82
  @token_stream = streams[:token]
68
83
  @agent_event_stream = streams[:agent]
69
84
  @tribunal_event_stream = streams[:tribunal]
@@ -155,7 +170,7 @@ module ActiveHarness
155
170
  agent_streams = { token: @token_stream, agent: @agent_event_stream }.compact
156
171
  @agents.map do |agent|
157
172
  if agent.is_a?(Class)
158
- agent.new(input: @input, context: @context.dup, streams: agent_streams)
173
+ agent.new(input: @input, context: @context.dup, params: @params, streams: agent_streams)
159
174
  else
160
175
  agent.input = @input if @input
161
176
  agent.instance_variable_set(:@token_stream, @token_stream) if @token_stream
@@ -1,5 +1,6 @@
1
1
  require_relative "active_harness/configuration"
2
2
  require_relative "active_harness/core/errors"
3
+ require_relative "active_harness/core/hooks"
3
4
  require_relative "active_harness/result"
4
5
  require_relative "active_harness/http/client"
5
6
  require_relative "active_harness/http/streaming_client"
@@ -29,7 +30,7 @@ require_relative "active_harness/pipeline"
29
30
  require_relative "active_harness/railtie" if defined?(Rails::Railtie)
30
31
 
31
32
  module ActiveHarness
32
- VERSION = "0.2.20"
33
+ VERSION = "0.2.22"
33
34
 
34
35
  class << self
35
36
  # Configure ActiveHarness.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.20
4
+ version: 0.2.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - the-teacher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -42,6 +42,7 @@ files:
42
42
  - lib/active_harness/agent/providers.rb
43
43
  - lib/active_harness/configuration.rb
44
44
  - lib/active_harness/core/errors.rb
45
+ - lib/active_harness/core/hooks.rb
45
46
  - lib/active_harness/costs.rb
46
47
  - lib/active_harness/data/models.json
47
48
  - lib/active_harness/http/client.rb
@@ -51,6 +52,7 @@ files:
51
52
  - lib/active_harness/memory/adapter/base.rb
52
53
  - lib/active_harness/memory/adapter/file.rb
53
54
  - lib/active_harness/pipeline.rb
55
+ - lib/active_harness/pipeline/hooks.rb
54
56
  - lib/active_harness/pipeline/step.rb
55
57
  - lib/active_harness/providers/PROVIDER_CONTRACT.md
56
58
  - lib/active_harness/providers/anthropic.rb