phronomy 0.14.0 → 0.15.0
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/CHANGELOG.md +65 -0
- data/README.md +236 -57
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +500 -411
- data/lib/phronomy/agent/context/capability/base.rb +51 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +42 -6
- data/lib/phronomy/engine/event_loop.rb +269 -112
- data/lib/phronomy/engine/fsm_session.rb +180 -142
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +2 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/tools/mcp.rb +25 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +54 -102
- data/lib/phronomy/workflow_runner.rb +238 -300
- data/lib/phronomy.rb +6 -4
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +18 -7
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -352
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
|
@@ -1,118 +1,81 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module Phronomy
|
|
4
|
-
# Implements the Generator-Verifier multi-agent coordination pattern
|
|
5
|
-
# (Anthropic blog, Pattern 1): a generator agent produces an
|
|
6
|
-
# answer while a verifier agent evaluates its quality.
|
|
7
|
-
#
|
|
8
|
-
# @see https://claude.com/blog/multi-agent-coordination-patterns
|
|
9
|
-
#
|
|
10
|
-
# All prompt construction and result parsing are provided by the caller,
|
|
11
|
-
# giving full control over the LLM dialogue.
|
|
12
|
-
# The generator and verifier agents are configurable, and the pipeline
|
|
13
|
-
# retries until confidence passes the threshold or max iterations are reached.
|
|
14
|
-
#
|
|
15
|
-
# @example Basic usage with custom prompt builders
|
|
16
|
-
# pipeline = Phronomy::GeneratorVerifier.new(
|
|
17
|
-
# draft_agent: MyDraftAgent,
|
|
18
|
-
# review_agent: MyReviewAgent,
|
|
19
|
-
# draft_prompt_builder: ->(input, feedback) { "Question: #{input}" },
|
|
20
|
-
# review_prompt_builder: ->(input, draft, citations) { "Review: #{draft}" }
|
|
21
|
-
# )
|
|
22
|
-
# result = pipeline.invoke("What is the refund policy?")
|
|
23
|
-
# puts result.output # the final answer string
|
|
24
|
-
# puts result.trusted? # true when confidence >= threshold
|
|
6
|
+
# Implements the Generator-Verifier multi-agent coordination pattern.
|
|
25
7
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# ...,
|
|
29
|
-
# draft_result_parser: ->(text) { my_parse_draft(text) },
|
|
30
|
-
# review_result_parser: ->(text) { my_parse_review(text) }
|
|
31
|
-
# )
|
|
32
|
-
#
|
|
33
|
-
# @example Raising on low confidence
|
|
34
|
-
# pipeline = Phronomy::GeneratorVerifier.new(
|
|
35
|
-
# ...,
|
|
36
|
-
# raise_if_untrusted: true
|
|
37
|
-
# )
|
|
38
|
-
# begin
|
|
39
|
-
# result = pipeline.invoke("question")
|
|
40
|
-
# rescue Phronomy::LowConfidenceError => e
|
|
41
|
-
# puts "Untrusted: #{e.result.confidence}"
|
|
42
|
-
# end
|
|
8
|
+
# Agent completion is integrated through application-defined Workflow events;
|
|
9
|
+
# Workflow entry actions do not return or await Agent Tasks.
|
|
43
10
|
class GeneratorVerifier
|
|
44
|
-
# Default confidence threshold for trusting an answer.
|
|
45
11
|
DEFAULT_CONFIDENCE_THRESHOLD = 0.7
|
|
46
|
-
|
|
47
|
-
# Default maximum draft-review cycles before returning best effort.
|
|
48
12
|
DEFAULT_MAX_ITERATIONS = 3
|
|
49
13
|
|
|
50
|
-
# Immutable value object returned by {GeneratorVerifier#invoke}.
|
|
51
|
-
#
|
|
52
|
-
# @!attribute [r] output
|
|
53
|
-
# @return [String] the final answer text
|
|
54
|
-
# @!attribute [r] confidence
|
|
55
|
-
# @return [Float] combined confidence score (0.0–1.0)
|
|
56
|
-
# @!attribute [r] citations
|
|
57
|
-
# @return [Array<Hash>] [{source:, excerpt:}, ...]
|
|
58
|
-
#
|
|
59
|
-
# **WARNING**: These citations are extracted from the LLM's own response
|
|
60
|
-
# and are **not** verified against any external knowledge base or URL.
|
|
61
|
-
# Do not treat them as authoritative without independent verification.
|
|
62
|
-
# @!attribute [r] iterations
|
|
63
|
-
# @return [Integer] number of draft-review cycles executed
|
|
64
|
-
# @!attribute [r] review_notes
|
|
65
|
-
# @return [Array<String>] reviewer feedback for each cycle
|
|
66
|
-
# @!attribute [r] trusted
|
|
67
|
-
# @return [Boolean] true when confidence >= threshold
|
|
68
14
|
Result = Struct.new(
|
|
69
|
-
:output,
|
|
15
|
+
:output,
|
|
16
|
+
:confidence,
|
|
17
|
+
:citations,
|
|
18
|
+
:iterations,
|
|
19
|
+
:review_notes,
|
|
20
|
+
:trusted
|
|
70
21
|
) do
|
|
71
|
-
# @return [Boolean] true when confidence >= threshold
|
|
72
22
|
alias_method :trusted?, :trusted
|
|
73
23
|
end
|
|
74
24
|
|
|
75
|
-
# Internal graph state — not part of the public API.
|
|
76
|
-
# @private
|
|
77
25
|
class PipelineState
|
|
78
26
|
include Phronomy::WorkflowContext
|
|
79
27
|
|
|
80
28
|
field :input, type: :replace, default: -> { "" }
|
|
81
|
-
field :draft, type: :replace, default: -> {}
|
|
29
|
+
field :draft, type: :replace, default: -> { {} }
|
|
82
30
|
field :self_score, type: :replace, default: -> { 0.0 }
|
|
83
31
|
field :review_score, type: :replace, default: -> { 0.0 }
|
|
84
32
|
field :citations, type: :replace, default: -> { [] }
|
|
85
33
|
field :review_notes, type: :append, default: -> { [] }
|
|
86
34
|
field :iteration, type: :replace, default: -> { 0 }
|
|
87
35
|
field :approved, type: :replace, default: -> { false }
|
|
88
|
-
field :output, type: :replace, default: -> {}
|
|
36
|
+
field :output, type: :replace, default: -> { {} }
|
|
37
|
+
field :draft_request_id, type: :replace, default: nil
|
|
38
|
+
field :review_request_id, type: :replace, default: nil
|
|
39
|
+
field :pipeline_error, type: :replace, default: nil
|
|
40
|
+
|
|
41
|
+
# Application-level event interpretation. Correlation IDs belong to this
|
|
42
|
+
# Pipeline rather than to the generic FSMSession.
|
|
43
|
+
def handle_fsm_event(event)
|
|
44
|
+
case event.type
|
|
45
|
+
when :draft_completed
|
|
46
|
+
return :consume unless event.payload[:request_id] == draft_request_id
|
|
47
|
+
|
|
48
|
+
self.draft = event.payload[:draft]
|
|
49
|
+
self.self_score = event.payload[:self_score]
|
|
50
|
+
self.citations = event.payload[:citations]
|
|
51
|
+
self.iteration = iteration + 1
|
|
52
|
+
self.draft_request_id = nil
|
|
53
|
+
self.pipeline_error = nil
|
|
54
|
+
when :review_completed
|
|
55
|
+
return :consume unless event.payload[:request_id] == review_request_id
|
|
56
|
+
|
|
57
|
+
self.review_score = event.payload[:review_score]
|
|
58
|
+
self.approved = event.payload[:approved]
|
|
59
|
+
self.review_notes = review_notes + [event.payload[:feedback]]
|
|
60
|
+
self.review_request_id = nil
|
|
61
|
+
self.pipeline_error = nil
|
|
62
|
+
when :draft_failed
|
|
63
|
+
return :consume unless event.payload[:request_id] == draft_request_id
|
|
64
|
+
|
|
65
|
+
self.pipeline_error = event.payload[:error]
|
|
66
|
+
self.draft_request_id = nil
|
|
67
|
+
when :review_failed
|
|
68
|
+
return :consume unless event.payload[:request_id] == review_request_id
|
|
69
|
+
|
|
70
|
+
self.pipeline_error = event.payload[:error]
|
|
71
|
+
self.review_request_id = nil
|
|
72
|
+
end
|
|
73
|
+
false
|
|
74
|
+
end
|
|
89
75
|
end
|
|
90
76
|
|
|
91
77
|
private_constant :PipelineState
|
|
92
78
|
|
|
93
|
-
# @param draft_agent [Class] subclass of Phronomy::Agent::Base
|
|
94
|
-
# used to generate answer drafts
|
|
95
|
-
# @param review_agent [Class] subclass of Phronomy::Agent::Base
|
|
96
|
-
# used to evaluate each draft
|
|
97
|
-
# @param draft_prompt_builder [#call] +call(input, feedback)+ → String
|
|
98
|
-
# prompt for the generator. +feedback+ is nil on the first iteration and
|
|
99
|
-
# contains the reviewer's feedback string on subsequent iterations.
|
|
100
|
-
# @param review_prompt_builder [#call] +call(input, draft, citations)+ → String
|
|
101
|
-
# prompt for the verifier. +citations+ is an Array of Hashes.
|
|
102
|
-
# @param draft_result_parser [#call, nil] +call(text)+ → Hash with
|
|
103
|
-
# +:answer+, +:confidence+, and +:citations+ keys. Defaults to JSON parsing
|
|
104
|
-
# with a safe fallback when the response cannot be parsed.
|
|
105
|
-
# @param review_result_parser [#call, nil] +call(text)+ → Hash with
|
|
106
|
-
# +:approved+, +:score+, and +:feedback+ keys. Defaults to JSON parsing
|
|
107
|
-
# with a safe fallback.
|
|
108
|
-
# @param confidence_threshold [Float] minimum combined confidence to
|
|
109
|
-
# trust an answer (default: 0.7)
|
|
110
|
-
# @param max_iterations [Integer] maximum draft-review cycles
|
|
111
|
-
# before returning the best-effort answer (default: 3)
|
|
112
|
-
# @param raise_if_untrusted [Boolean] when +true+, raises
|
|
113
|
-
# {Phronomy::LowConfidenceError} if the final result does not meet the
|
|
114
|
-
# confidence threshold (default: false)
|
|
115
|
-
# @api private
|
|
116
79
|
def initialize(
|
|
117
80
|
draft_agent:,
|
|
118
81
|
review_agent:,
|
|
@@ -128,25 +91,18 @@ module Phronomy
|
|
|
128
91
|
@review_agent_class = review_agent
|
|
129
92
|
@draft_prompt_builder = draft_prompt_builder
|
|
130
93
|
@review_prompt_builder = review_prompt_builder
|
|
131
|
-
@draft_result_parser =
|
|
132
|
-
|
|
94
|
+
@draft_result_parser =
|
|
95
|
+
draft_result_parser || method(:default_parse_draft)
|
|
96
|
+
@review_result_parser =
|
|
97
|
+
review_result_parser || method(:default_parse_review)
|
|
133
98
|
@threshold = confidence_threshold.to_f
|
|
134
99
|
@max_iterations = max_iterations.to_i
|
|
135
100
|
@raise_if_untrusted = raise_if_untrusted
|
|
136
101
|
@compiled_workflow = nil
|
|
137
102
|
end
|
|
138
103
|
|
|
139
|
-
# Run the generator-verifier pipeline.
|
|
140
|
-
#
|
|
141
|
-
# @param input [String] the user question or task description
|
|
142
|
-
# @param config [Hash] forwarded to the underlying agents (e.g. thread_id)
|
|
143
|
-
# @return [Result]
|
|
144
|
-
# @raise [Phronomy::LowConfidenceError] when +raise_if_untrusted:+ is +true+
|
|
145
|
-
# and the result does not meet the confidence threshold
|
|
146
|
-
# @api private
|
|
147
104
|
def invoke(input, config: {})
|
|
148
|
-
|
|
149
|
-
state = app.invoke({input: input}, config: config)
|
|
105
|
+
state = compiled_workflow.invoke({input: input}, config: config)
|
|
150
106
|
confidence = combined_confidence(state)
|
|
151
107
|
trusted = confidence >= @threshold
|
|
152
108
|
result = Result.new(
|
|
@@ -157,14 +113,19 @@ module Phronomy
|
|
|
157
113
|
review_notes: state.review_notes,
|
|
158
114
|
trusted: trusted
|
|
159
115
|
)
|
|
160
|
-
|
|
116
|
+
if @raise_if_untrusted && !trusted
|
|
117
|
+
raise LowConfidenceError.new(result)
|
|
118
|
+
end
|
|
161
119
|
result
|
|
162
120
|
end
|
|
163
121
|
|
|
164
122
|
private
|
|
165
123
|
|
|
166
124
|
def combined_confidence(state)
|
|
167
|
-
[
|
|
125
|
+
[
|
|
126
|
+
(state.self_score || 0.0).to_f,
|
|
127
|
+
(state.review_score || 0.0).to_f
|
|
128
|
+
].min
|
|
168
129
|
end
|
|
169
130
|
|
|
170
131
|
def compiled_workflow
|
|
@@ -175,86 +136,236 @@ module Phronomy
|
|
|
175
136
|
draft_agent = @draft_agent_class.new
|
|
176
137
|
review_agent = @review_agent_class.new
|
|
177
138
|
threshold = @threshold
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
139
|
+
max_iterations = @max_iterations
|
|
140
|
+
draft_prompt_builder = @draft_prompt_builder
|
|
141
|
+
review_prompt_builder = @review_prompt_builder
|
|
142
|
+
draft_result_parser = @draft_result_parser
|
|
143
|
+
review_result_parser = @review_result_parser
|
|
183
144
|
pipeline = self
|
|
145
|
+
workflow = nil
|
|
184
146
|
|
|
185
|
-
|
|
147
|
+
# workflow is assigned after define so closures captured by reference see the result.
|
|
148
|
+
workflow = Phronomy::Workflow.define(PipelineState) do
|
|
186
149
|
initial :draft
|
|
187
150
|
|
|
188
151
|
state :draft
|
|
189
152
|
state :review
|
|
190
153
|
state :finalize
|
|
154
|
+
state :failed
|
|
191
155
|
|
|
192
156
|
entry :draft, ->(state) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
157
|
+
request_id = SecureRandom.uuid
|
|
158
|
+
next_state = state.merge(draft_request_id: request_id)
|
|
159
|
+
feedback = next_state.review_notes.last
|
|
160
|
+
prompt = draft_prompt_builder.call(next_state.input, feedback)
|
|
161
|
+
|
|
162
|
+
draft_agent.invoke_async(
|
|
163
|
+
prompt,
|
|
164
|
+
on_event: ->(agent_event) {
|
|
165
|
+
case agent_event.type
|
|
166
|
+
when :done
|
|
167
|
+
begin
|
|
168
|
+
parsed = draft_result_parser.call(
|
|
169
|
+
agent_event.payload[:output]
|
|
170
|
+
)
|
|
171
|
+
workflow.signal(
|
|
172
|
+
thread_id: next_state.thread_id,
|
|
173
|
+
event: :draft_completed,
|
|
174
|
+
payload: {
|
|
175
|
+
request_id: request_id,
|
|
176
|
+
draft: parsed[:answer].to_s,
|
|
177
|
+
self_score: pipeline.__send__(
|
|
178
|
+
:clamp,
|
|
179
|
+
parsed[:confidence]
|
|
180
|
+
),
|
|
181
|
+
citations: pipeline.__send__(
|
|
182
|
+
:normalize_citations,
|
|
183
|
+
parsed[:citations]
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
rescue => error
|
|
188
|
+
workflow.signal(
|
|
189
|
+
thread_id: next_state.thread_id,
|
|
190
|
+
event: :draft_failed,
|
|
191
|
+
payload: {
|
|
192
|
+
request_id: request_id,
|
|
193
|
+
error: error
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
when :error, :timeout, :cancelled
|
|
198
|
+
workflow.signal(
|
|
199
|
+
thread_id: next_state.thread_id,
|
|
200
|
+
event: :draft_failed,
|
|
201
|
+
payload: {
|
|
202
|
+
request_id: request_id,
|
|
203
|
+
error:
|
|
204
|
+
agent_event.payload[:error] ||
|
|
205
|
+
Phronomy::Error.new(
|
|
206
|
+
"Draft Agent ended with #{agent_event.type}"
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
when :approval_required
|
|
211
|
+
workflow.signal(
|
|
212
|
+
thread_id: next_state.thread_id,
|
|
213
|
+
event: :draft_failed,
|
|
214
|
+
payload: {
|
|
215
|
+
request_id: request_id,
|
|
216
|
+
error: Phronomy::Error.new(
|
|
217
|
+
"GeneratorVerifier draft Agent suspended for approval"
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
next_state
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
entry :review, ->(state) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
request_id = SecureRandom.uuid
|
|
230
|
+
next_state = state.merge(review_request_id: request_id)
|
|
231
|
+
prompt = review_prompt_builder.call(
|
|
232
|
+
next_state.input,
|
|
233
|
+
next_state.draft,
|
|
234
|
+
next_state.citations
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
review_agent.invoke_async(
|
|
238
|
+
prompt,
|
|
239
|
+
on_event: ->(agent_event) {
|
|
240
|
+
case agent_event.type
|
|
241
|
+
when :done
|
|
242
|
+
begin
|
|
243
|
+
parsed = review_result_parser.call(
|
|
244
|
+
agent_event.payload[:output]
|
|
245
|
+
)
|
|
246
|
+
workflow.signal(
|
|
247
|
+
thread_id: next_state.thread_id,
|
|
248
|
+
event: :review_completed,
|
|
249
|
+
payload: {
|
|
250
|
+
request_id: request_id,
|
|
251
|
+
review_score: pipeline.__send__(
|
|
252
|
+
:clamp,
|
|
253
|
+
parsed[:score]
|
|
254
|
+
),
|
|
255
|
+
approved: parsed[:approved] == true,
|
|
256
|
+
feedback: parsed[:feedback].to_s
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
rescue => error
|
|
260
|
+
workflow.signal(
|
|
261
|
+
thread_id: next_state.thread_id,
|
|
262
|
+
event: :review_failed,
|
|
263
|
+
payload: {
|
|
264
|
+
request_id: request_id,
|
|
265
|
+
error: error
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
when :error, :timeout, :cancelled
|
|
270
|
+
workflow.signal(
|
|
271
|
+
thread_id: next_state.thread_id,
|
|
272
|
+
event: :review_failed,
|
|
273
|
+
payload: {
|
|
274
|
+
request_id: request_id,
|
|
275
|
+
error:
|
|
276
|
+
agent_event.payload[:error] ||
|
|
277
|
+
Phronomy::Error.new(
|
|
278
|
+
"Review Agent ended with #{agent_event.type}"
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
when :approval_required
|
|
283
|
+
workflow.signal(
|
|
284
|
+
thread_id: next_state.thread_id,
|
|
285
|
+
event: :review_failed,
|
|
286
|
+
payload: {
|
|
287
|
+
request_id: request_id,
|
|
288
|
+
error: Phronomy::Error.new(
|
|
289
|
+
"GeneratorVerifier review Agent suspended for approval"
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
next_state
|
|
219
298
|
}
|
|
220
299
|
|
|
221
|
-
entry :finalize, ->(state) {
|
|
300
|
+
entry :finalize, ->(state) {
|
|
301
|
+
state.output = state.draft
|
|
302
|
+
state
|
|
303
|
+
}
|
|
222
304
|
|
|
223
|
-
|
|
224
|
-
|
|
305
|
+
entry :failed, ->(state) {
|
|
306
|
+
raise(
|
|
307
|
+
state.pipeline_error ||
|
|
308
|
+
Phronomy::Error.new("GeneratorVerifier Agent operation failed")
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
transition from: :draft, on: :draft_completed, to: :review
|
|
313
|
+
transition from: :draft, on: :draft_failed, to: :failed
|
|
225
314
|
|
|
226
315
|
transition from: :review,
|
|
316
|
+
on: :review_completed,
|
|
227
317
|
guard: ->(state) {
|
|
228
|
-
confidence = [
|
|
229
|
-
|
|
318
|
+
confidence = [
|
|
319
|
+
state.self_score || 0.0,
|
|
320
|
+
state.review_score || 0.0
|
|
321
|
+
].min
|
|
322
|
+
(confidence >= threshold && state.approved) ||
|
|
323
|
+
state.iteration >= max_iterations
|
|
230
324
|
},
|
|
231
325
|
to: :finalize
|
|
232
|
-
transition from: :review,
|
|
326
|
+
transition from: :review,
|
|
327
|
+
on: :review_completed,
|
|
328
|
+
to: :draft
|
|
329
|
+
transition from: :review,
|
|
330
|
+
on: :review_failed,
|
|
331
|
+
to: :failed
|
|
332
|
+
|
|
333
|
+
transition from: :finalize, to: :__finish__
|
|
233
334
|
end
|
|
234
335
|
end
|
|
235
336
|
|
|
236
337
|
def default_parse_draft(text)
|
|
237
338
|
json_parser.parse(text)
|
|
238
339
|
rescue Phronomy::ParseError
|
|
239
|
-
{
|
|
340
|
+
{
|
|
341
|
+
answer: text.to_s,
|
|
342
|
+
confidence: 0.0,
|
|
343
|
+
citations: []
|
|
344
|
+
}
|
|
240
345
|
end
|
|
241
346
|
|
|
242
347
|
def default_parse_review(text)
|
|
243
348
|
json_parser.parse(text)
|
|
244
349
|
rescue Phronomy::ParseError
|
|
245
|
-
{
|
|
350
|
+
{
|
|
351
|
+
approved: false,
|
|
352
|
+
score: 0.0,
|
|
353
|
+
feedback: "Review output could not be parsed: #{text}"
|
|
354
|
+
}
|
|
246
355
|
end
|
|
247
356
|
|
|
248
357
|
def json_parser
|
|
249
358
|
@json_parser ||= Phronomy::OutputParser::JsonParser.new
|
|
250
359
|
end
|
|
251
360
|
|
|
252
|
-
def clamp(
|
|
253
|
-
|
|
361
|
+
def clamp(value)
|
|
362
|
+
value.to_f.clamp(0.0, 1.0)
|
|
254
363
|
end
|
|
255
364
|
|
|
256
365
|
def normalize_citations(raw)
|
|
257
|
-
Array(raw).filter_map
|
|
366
|
+
Array(raw).filter_map do |citation|
|
|
367
|
+
citation.is_a?(Hash) ? citation.transform_keys(&:to_sym) : nil
|
|
368
|
+
end
|
|
258
369
|
end
|
|
259
370
|
end
|
|
260
371
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Raised when a synchronous FSM entry action returns Phronomy::Task.
|
|
5
|
+
#
|
|
6
|
+
# Entry actions are Run-to-Completion callbacks. They may start asynchronous
|
|
7
|
+
# work, but completion must return through a later explicit event.
|
|
8
|
+
class InvalidAsyncEntryActionError < InvalidAsyncWorkflowActionError; end
|
|
9
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Raised when a synchronous Workflow transition action returns Phronomy::Task.
|
|
5
|
+
#
|
|
6
|
+
# Transition actions are Run-to-Completion callbacks. They may start
|
|
7
|
+
# asynchronous work, but completion must return through a later explicit event.
|
|
8
|
+
class InvalidAsyncTransitionActionError <
|
|
9
|
+
InvalidAsyncWorkflowActionError
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Base error for synchronous Workflow callbacks that return Phronomy::Task.
|
|
5
|
+
#
|
|
6
|
+
# Workflow callbacks may start asynchronous work, but they must return
|
|
7
|
+
# synchronously and deliver completion through a later explicit event.
|
|
8
|
+
class InvalidAsyncWorkflowActionError < Error; end
|
|
9
|
+
end
|
|
@@ -10,9 +10,8 @@ module Phronomy
|
|
|
10
10
|
#
|
|
11
11
|
# @example Build a context for a new agent invocation
|
|
12
12
|
# ctx = Phronomy::InvocationContext.new(
|
|
13
|
-
# thread_id:
|
|
14
|
-
# cancellation_token: Phronomy::Concurrency::CancellationToken.timeout_after(30)
|
|
15
|
-
# max_parallel_tools: 5
|
|
13
|
+
# thread_id: "conv-123",
|
|
14
|
+
# cancellation_token: Phronomy::Concurrency::CancellationToken.timeout_after(30)
|
|
16
15
|
# )
|
|
17
16
|
# agent.invoke("Hello", invocation_context: ctx)
|
|
18
17
|
class InvocationContext
|
|
@@ -37,18 +36,13 @@ module Phronomy
|
|
|
37
36
|
# @return [Integer, nil] max tokens the agent may consume this invocation
|
|
38
37
|
attr_reader :token_budget
|
|
39
38
|
|
|
40
|
-
# @return [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# @return [Object, nil] approval policy applied before write-scope tools
|
|
39
|
+
# @return [#call, nil] invocation-specific Tool approval policy. The callable
|
|
40
|
+
# receives Phronomy::Agent::ApprovalEvaluationRequest.
|
|
44
41
|
attr_reader :approval_policy
|
|
45
42
|
|
|
46
43
|
# @return [Object, nil] redaction policy applied to tool args / results
|
|
47
44
|
attr_reader :redaction_policy
|
|
48
45
|
|
|
49
|
-
# @return [Hash, nil] per-provider concurrency / rate-limit overrides
|
|
50
|
-
attr_reader :provider_limits
|
|
51
|
-
|
|
52
46
|
# @return [String, nil] unique identifier for this task in the trace tree
|
|
53
47
|
attr_reader :task_id
|
|
54
48
|
|
|
@@ -62,10 +56,8 @@ module Phronomy
|
|
|
62
56
|
# @param deadline [Deadline, nil]
|
|
63
57
|
# @param tracer_span [Object, nil]
|
|
64
58
|
# @param token_budget [Integer, nil]
|
|
65
|
-
# @param
|
|
66
|
-
# @param approval_policy [Object, nil]
|
|
59
|
+
# @param approval_policy [#call, nil] invocation-specific Tool approval policy
|
|
67
60
|
# @param redaction_policy [Object, nil]
|
|
68
|
-
# @param provider_limits [Hash, nil]
|
|
69
61
|
# @param task_id [String, nil]
|
|
70
62
|
# @param parent_task_id [String, nil]
|
|
71
63
|
# @api private
|
|
@@ -77,10 +69,8 @@ module Phronomy
|
|
|
77
69
|
deadline: nil,
|
|
78
70
|
tracer_span: nil,
|
|
79
71
|
token_budget: nil,
|
|
80
|
-
max_parallel_tools: 10,
|
|
81
72
|
approval_policy: nil,
|
|
82
73
|
redaction_policy: nil,
|
|
83
|
-
provider_limits: nil,
|
|
84
74
|
task_id: nil,
|
|
85
75
|
parent_task_id: nil
|
|
86
76
|
)
|
|
@@ -91,10 +81,8 @@ module Phronomy
|
|
|
91
81
|
@deadline = deadline
|
|
92
82
|
@tracer_span = tracer_span
|
|
93
83
|
@token_budget = token_budget
|
|
94
|
-
@max_parallel_tools = max_parallel_tools
|
|
95
84
|
@approval_policy = approval_policy
|
|
96
85
|
@redaction_policy = redaction_policy
|
|
97
|
-
@provider_limits = provider_limits
|
|
98
86
|
@task_id = task_id
|
|
99
87
|
@parent_task_id = parent_task_id
|
|
100
88
|
end
|
|
@@ -114,10 +102,8 @@ module Phronomy
|
|
|
114
102
|
deadline: overrides.fetch(:deadline, @deadline),
|
|
115
103
|
tracer_span: overrides.fetch(:tracer_span, @tracer_span),
|
|
116
104
|
token_budget: overrides.fetch(:token_budget, @token_budget),
|
|
117
|
-
max_parallel_tools: overrides.fetch(:max_parallel_tools, @max_parallel_tools),
|
|
118
105
|
approval_policy: overrides.fetch(:approval_policy, @approval_policy),
|
|
119
106
|
redaction_policy: overrides.fetch(:redaction_policy, @redaction_policy),
|
|
120
|
-
provider_limits: overrides.fetch(:provider_limits, @provider_limits),
|
|
121
107
|
task_id: overrides.fetch(:task_id, @task_id),
|
|
122
108
|
parent_task_id: overrides.fetch(:parent_task_id, @parent_task_id)
|
|
123
109
|
)
|