active_harness 0.2.38 → 0.2.40
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 +4 -4
- data/lib/active_harness/agent/image.rb +4 -0
- data/lib/active_harness/agent/providers.rb +12 -3
- data/lib/active_harness/memory.rb +11 -4
- data/lib/active_harness/pipeline/README.md +1 -1
- data/lib/active_harness/pipeline/step.rb +13 -9
- data/lib/active_harness/pipeline.rb +81 -12
- data/lib/active_harness/tribunal.rb +7 -1
- data/lib/active_harness.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4246d31550942e31c26ec9a708f49aae5b2e0f385a32aeaa06d6d6a75f2849ee
|
|
4
|
+
data.tar.gz: d7f12a75e7f72e3d004bfd3b64a094e99a082992fd0077e69253bddf97b919f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81cf0f7cb7f5f54c2a996097e34461f724bd66da920db0cee57cc84f558f8430bc83ad1e914c62a56f3493e890c993c9f2dd9408c161563ae8c161f5ac03e4d5
|
|
7
|
+
data.tar.gz: e144ed04675d274af6d66157a2f733e1cb65342adc7312b10428e9afc3c135b64af616218d6bb9ddbfb36948efd0e83b817f36654523f910c51d4797e07674e6
|
|
@@ -7,6 +7,10 @@ module ActiveHarness
|
|
|
7
7
|
# image true
|
|
8
8
|
# size "1024x1024"
|
|
9
9
|
#
|
|
10
|
+
# # Optional: base style/formatting guidance, prepended to @input
|
|
11
|
+
# # to form the final image prompt (see Agent#build_image_prompt).
|
|
12
|
+
# system_prompt "Watercolor style, soft pastel colors, no text overlays."
|
|
13
|
+
#
|
|
10
14
|
# model do
|
|
11
15
|
# use provider: :openrouter, model: "openai/gpt-5-image-mini"
|
|
12
16
|
# fallback provider: :openai, model: "gpt-image-1"
|
|
@@ -47,7 +47,7 @@ module ActiveHarness
|
|
|
47
47
|
|
|
48
48
|
def attempt_model(entry, system_prompt)
|
|
49
49
|
return attempt_via_custom_llm(entry, system_prompt) if @config[:custom_llm_backend]
|
|
50
|
-
return attempt_image_model(entry)
|
|
50
|
+
return attempt_image_model(entry, system_prompt) if @config[:image]
|
|
51
51
|
|
|
52
52
|
provider = resolve_provider(entry[:provider])
|
|
53
53
|
messages = build_messages(system_prompt, @input)
|
|
@@ -58,17 +58,26 @@ module ActiveHarness
|
|
|
58
58
|
provider.call(**opts)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
def attempt_image_model(entry)
|
|
61
|
+
def attempt_image_model(entry, system_prompt)
|
|
62
62
|
factory = IMAGE_PROVIDERS[entry[:provider].to_sym]
|
|
63
63
|
raise ArgumentError, "Provider #{entry[:provider].inspect} does not support image generation. " \
|
|
64
64
|
"Supported image providers: #{IMAGE_PROVIDERS.keys.join(', ')}" unless factory
|
|
65
65
|
|
|
66
66
|
size = entry[:size] || @config[:image_size] || "1024x1024"
|
|
67
|
-
opts = { model: entry[:model], prompt: @input.to_s, size: size }
|
|
67
|
+
opts = { model: entry[:model], prompt: build_image_prompt(system_prompt, @input.to_s), size: size }
|
|
68
68
|
opts[:quality] = entry[:quality] if entry[:quality]
|
|
69
69
|
factory.call.call(**opts)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Image APIs take a single prompt string (no system/user role split), so the
|
|
73
|
+
# system prompt — e.g. base style/formatting guidance — is prepended to the
|
|
74
|
+
# user's input rather than sent as a separate message.
|
|
75
|
+
def build_image_prompt(system_prompt, input)
|
|
76
|
+
return input if system_prompt.nil? || system_prompt.to_s.strip.empty?
|
|
77
|
+
|
|
78
|
+
"#{system_prompt}\n\n#{input}"
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
def resolve_provider(name)
|
|
73
82
|
factory = PROVIDERS[name.to_sym]
|
|
74
83
|
raise ArgumentError, "Unknown provider: #{name.inspect}. Supported: #{PROVIDERS.keys.join(', ')}" unless factory
|
|
@@ -10,14 +10,21 @@ module ActiveHarness
|
|
|
10
10
|
# It does NOT automatically inject history into LLM messages.
|
|
11
11
|
# Injection is always manual — you control when and how context is used.
|
|
12
12
|
#
|
|
13
|
-
# --- Recording
|
|
13
|
+
# --- Recording ---
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# Passing a Memory object to Agent.call(memory:) does NOT by itself save
|
|
16
|
+
# anything — Agent never calls #load or #record on it. Loading and
|
|
17
|
+
# recording turns for a bare agent call is entirely manual (e.g. via
|
|
18
|
+
# before_call/after_call hooks calling @memory.load / @memory.record).
|
|
19
|
+
#
|
|
20
|
+
# Pipeline is the one place recording is automatic: when a Memory is
|
|
21
|
+
# passed to Pipeline#call, the pipeline itself calls #load before running
|
|
22
|
+
# steps and #record after a successful run — independent of any hooks the
|
|
23
|
+
# individual agents define.
|
|
17
24
|
#
|
|
18
25
|
# memory = ActiveHarness::Memory.new(session_id: "u42", depth: 8)
|
|
19
26
|
# SupportAgent.call(input: "Hello", memory: memory)
|
|
20
|
-
# # =>
|
|
27
|
+
# # => nothing is saved unless SupportAgent's own hooks record it
|
|
21
28
|
#
|
|
22
29
|
# --- Manual injection patterns ---
|
|
23
30
|
#
|
|
@@ -25,7 +25,7 @@ pipeline.call
|
|
|
25
25
|
|
|
26
26
|
pipeline.output # => final payload string (nil if stopped)
|
|
27
27
|
pipeline.stopped? # => false
|
|
28
|
-
pipeline.
|
|
28
|
+
pipeline.steps.to_a # => [[:translate, <TranslationAgent>, <Result>], [:injection_guard, <InjectionGuardAgent>, <Result>], ...]
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Step types
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
module ActiveHarness
|
|
2
2
|
class Pipeline
|
|
3
3
|
class Step
|
|
4
|
-
attr_reader :name, :
|
|
4
|
+
attr_reader :name, :executor
|
|
5
5
|
|
|
6
|
-
def initialize(name,
|
|
7
|
-
@name
|
|
8
|
-
@
|
|
9
|
-
@stop_if
|
|
6
|
+
def initialize(name, executor = nil, &block)
|
|
7
|
+
@name = name
|
|
8
|
+
@executor = executor
|
|
9
|
+
@stop_if = nil
|
|
10
10
|
instance_eval(&block) if block_given?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# DSL: use TranslationAgent
|
|
13
|
+
# DSL: use TranslationAgent / SafetyTribunal / NestedPipeline
|
|
14
14
|
def use(klass)
|
|
15
|
-
@
|
|
15
|
+
@executor = klass
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# DSL (inside block): stop_if ->(result) { ... }
|
|
@@ -35,9 +35,13 @@ module ActiveHarness
|
|
|
35
35
|
@transform_block
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
def lambda?
|
|
39
|
+
@executor.is_a?(Proc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True if the executor is a Tribunal subclass — tribunal steps do not update payload.
|
|
39
43
|
def tribunal?
|
|
40
|
-
@
|
|
44
|
+
@executor.is_a?(Class) && @executor <= ActiveHarness::Tribunal
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
# Returns true when this step should update the pipeline payload after execution.
|
|
@@ -29,7 +29,7 @@ module ActiveHarness
|
|
|
29
29
|
# pipeline.call
|
|
30
30
|
# pipeline.output # => final payload string (nil if stopped)
|
|
31
31
|
# pipeline.stopped? # => false
|
|
32
|
-
# pipeline.
|
|
32
|
+
# pipeline.steps { |name, result| ... } # iterate over completed steps
|
|
33
33
|
#
|
|
34
34
|
class Pipeline
|
|
35
35
|
# -------------------------------------------------------------------------
|
|
@@ -46,8 +46,8 @@ module ActiveHarness
|
|
|
46
46
|
# use InjectionGuardAgent
|
|
47
47
|
# stop_if ->(result) { result.processed["detected"] == true }
|
|
48
48
|
# end
|
|
49
|
-
def step(name,
|
|
50
|
-
pipeline_config[:steps] << Pipeline::Step.new(name,
|
|
49
|
+
def step(name, executor = nil, &block)
|
|
50
|
+
pipeline_config[:steps] << Pipeline::Step.new(name, executor, &block)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def pipeline_config
|
|
@@ -100,7 +100,6 @@ module ActiveHarness
|
|
|
100
100
|
:stopped_at,
|
|
101
101
|
:stop_reason,
|
|
102
102
|
:execution_time,
|
|
103
|
-
:step_results,
|
|
104
103
|
:context
|
|
105
104
|
attr_writer :context
|
|
106
105
|
attr_accessor :params
|
|
@@ -126,6 +125,7 @@ module ActiveHarness
|
|
|
126
125
|
@token = token
|
|
127
126
|
class_streams = self.class.pipeline_config[:streams] || {}
|
|
128
127
|
@stream = merge_stream(stream, class_streams)
|
|
128
|
+
@executors = build_executors
|
|
129
129
|
@step_results = {}
|
|
130
130
|
@stopped = false
|
|
131
131
|
@stopped_at = nil
|
|
@@ -134,10 +134,44 @@ module ActiveHarness
|
|
|
134
134
|
@output = nil
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
# Returns a hash of pre-created executor instances keyed by step name.
|
|
138
|
+
# Available immediately after new — before call — so instances can be
|
|
139
|
+
# configured (model lists, params, etc.) before execution begins.
|
|
140
|
+
#
|
|
141
|
+
# pipeline = SupportPipeline.new(input: "...")
|
|
142
|
+
# pipeline.executors[:translate].models.prepend(provider: :openai, model: "gpt-4.1")
|
|
143
|
+
# pipeline.executors[:guard].params = { strict: true }
|
|
144
|
+
# pipeline.call
|
|
145
|
+
def executors
|
|
146
|
+
@executors
|
|
147
|
+
end
|
|
148
|
+
|
|
137
149
|
def stopped?
|
|
138
150
|
@stopped
|
|
139
151
|
end
|
|
140
152
|
|
|
153
|
+
# Iterates over completed steps as (name, executor, result) tuples.
|
|
154
|
+
# Steps that did not run (pipeline stopped before reaching them) are skipped.
|
|
155
|
+
# Returns an Enumerator when called without a block.
|
|
156
|
+
#
|
|
157
|
+
# pipeline.steps { |name, executor, result| }
|
|
158
|
+
#
|
|
159
|
+
# name — step name symbol (:translate, :guard, …)
|
|
160
|
+
# executor — the instance that ran (TranslationAgent instance, …)
|
|
161
|
+
# result — Result struct (output, processed, usage, model, …)
|
|
162
|
+
#
|
|
163
|
+
# pipeline.steps.map { |name, executor, result| [name, result.output] }
|
|
164
|
+
# pipeline.steps.to_a
|
|
165
|
+
def steps
|
|
166
|
+
return enum_for(:steps) unless block_given?
|
|
167
|
+
self.class.pipeline_config[:steps].each do |step|
|
|
168
|
+
result = @step_results[step.name]
|
|
169
|
+
next unless result
|
|
170
|
+
yield step.name, @executors[step.name], result
|
|
171
|
+
end
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
141
175
|
# Wraps pipeline outcome into a Result so a pipeline can be used as a step
|
|
142
176
|
# inside another pipeline, matching the same interface as Agent and Tribunal.
|
|
143
177
|
#
|
|
@@ -153,7 +187,15 @@ module ActiveHarness
|
|
|
153
187
|
end
|
|
154
188
|
|
|
155
189
|
# Execute all steps sequentially. Returns self for chaining.
|
|
156
|
-
|
|
190
|
+
# Accepts optional input, token, stream to match the Agent/Tribunal call interface.
|
|
191
|
+
def call(input = nil, token: nil, stream: nil)
|
|
192
|
+
if input
|
|
193
|
+
@original_input = input
|
|
194
|
+
@payload = input
|
|
195
|
+
end
|
|
196
|
+
@token = token if token
|
|
197
|
+
@stream = stream if stream
|
|
198
|
+
|
|
157
199
|
config = self.class.pipeline_config
|
|
158
200
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
159
201
|
|
|
@@ -231,13 +273,40 @@ module ActiveHarness
|
|
|
231
273
|
end
|
|
232
274
|
|
|
233
275
|
def execute_step(step)
|
|
234
|
-
step.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
276
|
+
if step.lambda?
|
|
277
|
+
execute_lambda_step(step)
|
|
278
|
+
else
|
|
279
|
+
instance = @executors[step.name]
|
|
280
|
+
instance.context = @context.dup
|
|
281
|
+
instance.call(@payload).result
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def execute_lambda_step(step)
|
|
286
|
+
lam = step.executor
|
|
287
|
+
has_kw = lam.parameters.any? { |type, _| [:key, :keyreq, :keyrest].include?(type) }
|
|
288
|
+
result = has_kw ? lam.call(@payload, context: @context.dup, params: @params) : lam.call(@payload)
|
|
289
|
+
|
|
290
|
+
unless result.is_a?(Result)
|
|
291
|
+
raise ArgumentError,
|
|
292
|
+
"Lambda step :#{step.name} must return ActiveHarness::Result, got #{result.class}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
result
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def build_executors
|
|
299
|
+
self.class.pipeline_config[:steps].each_with_object({}) do |step, hash|
|
|
300
|
+
next if step.lambda?
|
|
301
|
+
|
|
302
|
+
hash[step.name] = step.executor.new(
|
|
303
|
+
input: @payload,
|
|
304
|
+
context: @context.dup,
|
|
305
|
+
params: @params,
|
|
306
|
+
token: @token,
|
|
307
|
+
stream: @stream
|
|
308
|
+
)
|
|
309
|
+
end
|
|
241
310
|
end
|
|
242
311
|
end
|
|
243
312
|
end
|
|
@@ -105,11 +105,17 @@ module ActiveHarness
|
|
|
105
105
|
# Run all agents in parallel, then compute the verdict.
|
|
106
106
|
# Returns self so calls can be chained: tribunal.call.verdict
|
|
107
107
|
#
|
|
108
|
+
# Accepts an optional input to update payload before running — matches
|
|
109
|
+
# the Agent#call(input) interface so tribunals work as pipeline executors.
|
|
110
|
+
#
|
|
108
111
|
# Behaviour on failure:
|
|
109
112
|
# - If some agents fail/timeout, their errors are in #errors and
|
|
110
113
|
# #results contains only successful results.
|
|
111
114
|
# - If ALL agents fail/timeout, raises Errors::AllAgentsFailed.
|
|
112
|
-
def call
|
|
115
|
+
def call(input = nil, token: nil, stream: nil)
|
|
116
|
+
@input = input if input
|
|
117
|
+
@token = token if token
|
|
118
|
+
@stream = stream if stream
|
|
113
119
|
agents = resolve_agents
|
|
114
120
|
fire(:before_call)
|
|
115
121
|
|
data/lib/active_harness.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.40
|
|
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-
|
|
11
|
+
date: 2026-07-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|