active_harness 0.2.28 → 0.2.30

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: ce3118f24bd7e48d024a4b5c14d0e01531e453be2f56479918759dc425bc932d
4
- data.tar.gz: 561d4d3f46b98a224d53ed964c319fbb1a73bffc5c6a3dd189d01e24d1af2427
3
+ metadata.gz: f5f759449bd21052f986bc092a621f2ce39cb4faa630ba1bc759419bc27aac37
4
+ data.tar.gz: 94e8abc9d0a0ca7ef877ea0f1e66ac68899236d7d33f10ac754f3541b3a29d1b
5
5
  SHA512:
6
- metadata.gz: 29f2a581e10290eb176a80e142a120ba5e9c9f0bc9e5decfacb7237fd11f35621bd35a64ebca2999399edd5f5865aec343edb8a11c8040e539329857b51c6d23
7
- data.tar.gz: 86ef4129befa8b92af78f5666a4f6346f9d9eba46f39e880cf9968d03b7339f6de9620a0b558d67571bd4650425da0ea3455b8251c356e366717c87233dc3516
6
+ metadata.gz: 8a9529f9391337660a325993adf7db17bd2f470f6b2e69cab256a1ac907616d0c76a5c5b4c56f9850399fbfb8d886b3f2d2c2241c6ceb66ea0d83cb0e2d4c724
7
+ data.tar.gz: e1ae97b30a763b761ae28d20b957670d5b8a774d249456cfd84da09830ce6ebc8661c2d19790a69ff2889ff585ce7af307beb13b5d46886708db041fd1c33d73
@@ -64,13 +64,31 @@ module ActiveHarness
64
64
  :cache_write_input_per_million,
65
65
  :context_window,
66
66
  :max_output_tokens,
67
+ :input_modalities,
68
+ :output_modalities,
67
69
  keyword_init: true
68
70
  ) do
71
+ # Returns capability tags derived from modality data and model id/name.
72
+ # Possible values: "vision", "pdf", "audio", "video", "imggen", "embed"
73
+ def categories
74
+ inp = input_modalities || []
75
+ out = output_modalities || []
76
+ cats = []
77
+ cats << "vision" if inp.include?("image")
78
+ cats << "pdf" if inp.include?("pdf")
79
+ cats << "audio" if inp.include?("audio") || out.include?("audio")
80
+ cats << "video" if inp.include?("video") || out.include?("video")
81
+ cats << "imggen" if out.include?("image")
82
+ cats << "embed" if id.to_s.match?(/embed/i) || name.to_s.match?(/embed/i)
83
+ cats
84
+ end
85
+
69
86
  def inspect
70
87
  parts = ["id=#{id.inspect}", "provider=#{provider.inspect}"]
71
88
  parts << "input=$#{input_per_million}/M" if input_per_million
72
89
  parts << "output=$#{output_per_million}/M" if output_per_million
73
90
  parts << "ctx=#{context_window}" if context_window
91
+ parts << "cats=#{categories.join(',')}" if categories.any?
74
92
  "#<ModelCost #{parts.join(' ')}>"
75
93
  end
76
94
  end
@@ -232,12 +250,15 @@ module ActiveHarness
232
250
  cache_write_input_per_million: cost[:cache_write]
233
251
  }.compact
234
252
 
253
+ mods = m[:modalities] || {}
235
254
  {
236
255
  id: m[:id],
237
256
  name: m[:name] || m[:id],
238
257
  provider: ah_provider,
239
258
  context_window: m[:context_window] || m.dig(:limit, :context),
240
259
  max_output_tokens: m[:max_output_tokens] || m.dig(:limit, :output),
260
+ input_modalities: Array(mods[:input]),
261
+ output_modalities: Array(mods[:output]),
241
262
  pricing: standard.any? ? { text_tokens: { standard: standard } } : {}
242
263
  }
243
264
  end
@@ -255,7 +276,9 @@ module ActiveHarness
255
276
  cache_read_input_per_million: standard[:cache_read_input_per_million],
256
277
  cache_write_input_per_million: standard[:cache_write_input_per_million],
257
278
  context_window: raw[:context_window],
258
- max_output_tokens: raw[:max_output_tokens]
279
+ max_output_tokens: raw[:max_output_tokens],
280
+ input_modalities: Array(raw[:input_modalities]),
281
+ output_modalities: Array(raw[:output_modalities])
259
282
  )
260
283
  end
261
284
 
@@ -84,6 +84,12 @@ module ActiveHarness
84
84
  async: false,
85
85
  **adapter_opts
86
86
  )
87
+ if self.class == Memory
88
+ raise NotImplementedError,
89
+ "Memory cannot be instantiated directly. " \
90
+ "Use Memory::JsonFile, Memory::Postgresql, or Memory::Sqlite."
91
+ end
92
+
87
93
  @session_id = session_id
88
94
  @depth = depth
89
95
  @enabled = enabled
@@ -16,7 +16,7 @@ class SupportPipeline < ActiveHarness::Pipeline
16
16
 
17
17
  step :safety_tribunal do
18
18
  use SafetyTribunal
19
- stop_if ->(result) { result.verdict == false }
19
+ stop_if ->(result) { result.processed["verdict"] == false }
20
20
  end
21
21
  end
22
22
 
@@ -21,15 +21,37 @@ module ActiveHarness
21
21
  lam ? @stop_if = lam : @stop_if
22
22
  end
23
23
 
24
+ # DSL: define how to extract the new payload from a result.
25
+ # When provided, the step always updates the payload — even if stop_if is also set.
26
+ # The block receives the Result and must return the new payload value.
27
+ #
28
+ # step :laundry do
29
+ # use PromptLaundryPipeline
30
+ # transform { |result| result.output }
31
+ # stop_if ->(result) { result.processed["stopped"] == true }
32
+ # end
33
+ def transform(&block)
34
+ @transform_block = block if block
35
+ @transform_block
36
+ end
37
+
24
38
  # True if agent_class is a Tribunal subclass — tribunal steps do not update payload.
25
39
  def tribunal?
26
40
  @agent_class.is_a?(Class) && @agent_class <= ActiveHarness::Tribunal
27
41
  end
28
42
 
29
- # Transform steps update payload to result.output after execution.
30
- # Guard steps (stop_if present) and tribunal steps leave payload unchanged.
43
+ # Returns true when this step should update the pipeline payload after execution.
44
+ # A step transforms when:
45
+ # - an explicit transform block is defined (overrides default), OR
46
+ # - no stop_if and not a tribunal (legacy default)
31
47
  def transform?
32
- !tribunal? && @stop_if.nil?
48
+ @transform_block ? true : (!tribunal? && @stop_if.nil?)
49
+ end
50
+
51
+ # Extract the new payload value from result.
52
+ # Uses the user-defined transform block when present; falls back to result.output.
53
+ def extract_payload(result)
54
+ @transform_block ? @transform_block.call(result) : result.output
33
55
  end
34
56
  end
35
57
  end
@@ -14,7 +14,7 @@ module ActiveHarness
14
14
  #
15
15
  # step :safety_tribunal do
16
16
  # use SafetyTribunal
17
- # stop_if ->(result) { result.verdict == false }
17
+ # stop_if ->(result) { result.processed["verdict"] == false }
18
18
  # end
19
19
  #
20
20
  # on :before_step do |step_name, payload| ... end
@@ -140,6 +140,20 @@ module ActiveHarness
140
140
  @stopped
141
141
  end
142
142
 
143
+ # Wraps pipeline outcome into a Result so a pipeline can be used as a step
144
+ # inside another pipeline, matching the same interface as Agent and Tribunal.
145
+ #
146
+ # output — final payload (nil when stopped)
147
+ # processed — { "stopped" => bool, "stopped_at" => step_name_string_or_nil }
148
+ def result
149
+ Result.new(
150
+ input: @original_input,
151
+ output: @output,
152
+ processed: { "stopped" => @stopped, "stopped_at" => @stopped_at&.to_s },
153
+ execution_time: @execution_time
154
+ )
155
+ end
156
+
143
157
  # Execute all steps sequentially. Returns self for chaining.
144
158
  def call
145
159
  config = self.class.pipeline_config
@@ -155,7 +169,7 @@ module ActiveHarness
155
169
 
156
170
  @step_results[step.name] = result
157
171
  @context[step.name] = result
158
- @payload = result.output if step.transform?
172
+ @payload = step.extract_payload(result) if step.transform?
159
173
 
160
174
  fire(:after_step, step.name, result, config)
161
175
  fire_step(:after_step, step.name, result, config)
@@ -210,7 +224,12 @@ module ActiveHarness
210
224
  end
211
225
 
212
226
  def execute_step(step)
213
- streams = { token: @token_stream, agent: @agent_event_stream, tribunal: @tribunal_event_stream }.compact
227
+ streams = {
228
+ token: @token_stream,
229
+ agent: @agent_event_stream,
230
+ tribunal: @tribunal_event_stream,
231
+ pipeline: @pipeline_event_stream
232
+ }.compact
214
233
  step.agent_class.new(
215
234
  input: @payload,
216
235
  context: @context.dup,
@@ -30,7 +30,7 @@ require_relative "active_harness/pipeline"
30
30
  require_relative "active_harness/railtie" if defined?(Rails::Railtie)
31
31
 
32
32
  module ActiveHarness
33
- VERSION = "0.2.28"
33
+ VERSION = "0.2.30"
34
34
 
35
35
  class << self
36
36
  # Configure ActiveHarness.
@@ -43,11 +43,12 @@ class AiSupportController < ApplicationController
43
43
  # Returns verdict: true (safe) or false (rejected).
44
44
  # ---------------------------------------------------------------------------
45
45
  def tribunal
46
- result = SupportGuardTribunal.call(input: params.require(:input))
46
+ tribunal = SupportGuardTribunal.new(input: params.require(:input))
47
+ tribunal.call
47
48
 
48
49
  render json: {
49
- verdict: result.verdict,
50
- time: result.execution_time
50
+ verdict: tribunal.verdict,
51
+ time: tribunal.execution_time
51
52
  }
52
53
  end
53
54
 
@@ -7,7 +7,7 @@ class SupportPipeline < ActiveHarness::Pipeline
7
7
  # Step 1 — GUARD: reject spam before spending tokens on an answer
8
8
  step :spam_guard do
9
9
  use SupportGuardTribunal
10
- stop_if ->(result) { result.verdict == false }
10
+ stop_if ->(result) { result.processed["verdict"] == false }
11
11
  end
12
12
 
13
13
  # Step 2 — RESPOND: generate the actual answer
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.28
4
+ version: 0.2.30
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-06-09 00:00:00.000000000 Z
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby