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
|
@@ -4,41 +4,20 @@ require "state_machines"
|
|
|
4
4
|
|
|
5
5
|
module Phronomy
|
|
6
6
|
class Workflow
|
|
7
|
-
# Builds the anonymous state-machine Class used by
|
|
8
|
-
# workflow phase transitions.
|
|
7
|
+
# Builds the anonymous state-machine Class used by WorkflowRunner.
|
|
9
8
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# Call {#build} to obtain the generated +Class+. The returned class responds
|
|
15
|
-
# to +#context+ / +#context=+ and +#async_pending+ / +#async_pending=+, and
|
|
16
|
-
# has a +state_machine :phase+ definition with all registered transitions and
|
|
17
|
-
# callbacks.
|
|
9
|
+
# This class compiles Workflow topology into state_machines declarations.
|
|
10
|
+
# It intentionally does not await Tasks, register completion callbacks,
|
|
11
|
+
# cancel external work, or interpret application event payloads.
|
|
18
12
|
#
|
|
19
13
|
# @api private
|
|
20
14
|
class PhaseMachineBuilder
|
|
21
|
-
# @param entry_point [Symbol] initial state for the phase machine
|
|
22
|
-
# @param declared_states [Array<Symbol>] all states declared in the workflow
|
|
23
|
-
# @param wait_state_names [Array<Symbol>] states that wait for external events
|
|
24
|
-
# @param external_events [Hash{Symbol => Array<Hash>}]
|
|
25
|
-
# +{ event_name => [{from:, to:, guard:}, ...] }+
|
|
26
|
-
# @param entry_actions [Hash{Symbol => Array<#call>}]
|
|
27
|
-
# +{ state_name => [callable, ...] }+
|
|
28
|
-
# @param action_timeouts [Hash{Symbol => Numeric}]
|
|
29
|
-
# +{ state_name => seconds }+
|
|
30
|
-
# @param auto_transitions [Array<Hash>]
|
|
31
|
-
# +[{ from:, to:, guard: }, ...]+ — all auto-fire transitions
|
|
32
|
-
# @param exit_actions [Hash{Symbol => Array<#call>}]
|
|
33
|
-
# +{ state_name => [callable, ...] }+
|
|
34
|
-
# @api private
|
|
35
15
|
def initialize(
|
|
36
16
|
entry_point:,
|
|
37
17
|
declared_states:,
|
|
38
18
|
wait_state_names:,
|
|
39
19
|
external_events:,
|
|
40
20
|
entry_actions:,
|
|
41
|
-
action_timeouts:,
|
|
42
21
|
auto_transitions:,
|
|
43
22
|
exit_actions:
|
|
44
23
|
)
|
|
@@ -47,215 +26,179 @@ module Phronomy
|
|
|
47
26
|
@wait_state_names = wait_state_names
|
|
48
27
|
@external_events = external_events
|
|
49
28
|
@entry_actions = entry_actions
|
|
50
|
-
@action_timeouts = action_timeouts
|
|
51
29
|
@auto_transitions = auto_transitions
|
|
52
30
|
@exit_actions = exit_actions
|
|
53
31
|
end
|
|
54
32
|
|
|
55
|
-
# Constructs and returns the anonymous phase-machine Class.
|
|
56
|
-
#
|
|
57
|
-
# @return [Class] an anonymous class with a +state_machine :phase+ definition
|
|
58
|
-
# @raise [ArgumentError] if state_machines raises during class construction
|
|
59
|
-
# @api private
|
|
60
33
|
def build
|
|
61
34
|
entry = @entry_point
|
|
62
35
|
all_states = (@declared_states + @wait_state_names + [:__end__]).uniq
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
36
|
+
auto_transitions = @auto_transitions
|
|
37
|
+
external_events = @external_events
|
|
38
|
+
entry_actions = @entry_actions
|
|
39
|
+
exit_actions = @exit_actions
|
|
40
|
+
condition_builder = method(:build_transition_condition)
|
|
41
|
+
entry_callback_builder = method(:build_entry_callback)
|
|
42
|
+
exit_callback_builder = method(:build_exit_callback)
|
|
43
|
+
transition_callback = build_transition_action_callback
|
|
69
44
|
|
|
70
45
|
Class.new do
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
attr_accessor :async_pending
|
|
78
|
-
|
|
79
|
-
# Invocation-local routing. FSMSession sets these on each machine
|
|
80
|
-
# instance; the compiled Class captures no Runtime-specific object.
|
|
81
|
-
attr_accessor :event_loop, :timer_queue_provider, :session_id
|
|
46
|
+
attr_accessor(
|
|
47
|
+
:context,
|
|
48
|
+
:current_event,
|
|
49
|
+
:selected_transition_action,
|
|
50
|
+
:selected_transition_metadata
|
|
51
|
+
)
|
|
82
52
|
|
|
83
53
|
state_machine :phase, initial: entry do
|
|
84
|
-
all_states.each { |
|
|
54
|
+
all_states.each { |state_name| state state_name }
|
|
85
55
|
|
|
86
|
-
# Auto-fire transitions: all auto transitions unified under :state_completed.
|
|
87
|
-
# Includes unguarded (unconditional) and guarded (conditional) transitions.
|
|
88
|
-
# Declaration order is preserved; guards are evaluated before unguarded fallbacks.
|
|
89
56
|
event :state_completed do
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
transition t[:from] => t[:to]
|
|
96
|
-
end
|
|
57
|
+
auto_transitions.each do |transition_definition|
|
|
58
|
+
transition(
|
|
59
|
+
transition_definition[:from] => transition_definition[:to],
|
|
60
|
+
:if => condition_builder.call(transition_definition)
|
|
61
|
+
)
|
|
97
62
|
end
|
|
98
63
|
end
|
|
99
64
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
else
|
|
108
|
-
transition t[:from] => t[:to]
|
|
109
|
-
end
|
|
65
|
+
external_events.each do |event_name, transitions|
|
|
66
|
+
event event_name do
|
|
67
|
+
transitions.each do |transition_definition|
|
|
68
|
+
transition(
|
|
69
|
+
transition_definition[:from] => transition_definition[:to],
|
|
70
|
+
:if => condition_builder.call(transition_definition)
|
|
71
|
+
)
|
|
110
72
|
end
|
|
111
73
|
end
|
|
112
74
|
end
|
|
113
75
|
|
|
114
|
-
|
|
115
|
-
# Each callable is registered as a separate callback; state_machines
|
|
116
|
-
# accumulates them and fires in declaration order.
|
|
117
|
-
# If the callable returns a WorkflowContext (e.g. via s.merge(...)),
|
|
118
|
-
# the returned context replaces the current one on the tracker.
|
|
119
|
-
entry_acts.each do |state_name, callables|
|
|
76
|
+
entry_actions.each do |state_name, callables|
|
|
120
77
|
callables.each do |callable|
|
|
121
|
-
|
|
122
|
-
|
|
78
|
+
after_transition(
|
|
79
|
+
to: state_name,
|
|
80
|
+
&entry_callback_builder.call(callable, state_name)
|
|
81
|
+
)
|
|
123
82
|
end
|
|
124
83
|
end
|
|
125
84
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
|
|
85
|
+
# Both source exit callbacks and the transition action are
|
|
86
|
+
# before_transition callbacks. Register exits first to preserve the
|
|
87
|
+
# required exit -> transition action -> entry ordering.
|
|
88
|
+
exit_actions.each do |state_name, callables|
|
|
130
89
|
callables.each do |callable|
|
|
131
|
-
before_transition
|
|
132
|
-
|
|
133
|
-
|
|
90
|
+
before_transition(
|
|
91
|
+
from: state_name,
|
|
92
|
+
&exit_callback_builder.call(callable, state_name)
|
|
93
|
+
)
|
|
134
94
|
end
|
|
135
95
|
end
|
|
96
|
+
|
|
97
|
+
before_transition(&transition_callback)
|
|
136
98
|
end
|
|
137
99
|
end
|
|
138
|
-
rescue =>
|
|
139
|
-
raise ArgumentError, "Failed to build phase machine: #{
|
|
100
|
+
rescue => error
|
|
101
|
+
raise ArgumentError, "Failed to build phase machine: #{error.message}"
|
|
140
102
|
end
|
|
141
103
|
|
|
142
104
|
private
|
|
143
105
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# @param timeout_secs [Numeric, nil] seconds before ActionTimeoutError
|
|
154
|
-
# @return [Proc]
|
|
155
|
-
# @api private
|
|
156
|
-
def build_entry_callback(callable, state_name, timeout_secs)
|
|
157
|
-
handle = method(:handle_entry_action_result)
|
|
106
|
+
def build_transition_condition(transition_definition)
|
|
107
|
+
guard = transition_definition[:guard]
|
|
108
|
+
action = transition_definition[:action]
|
|
109
|
+
metadata = {
|
|
110
|
+
from: transition_definition[:from],
|
|
111
|
+
event: transition_definition[:event],
|
|
112
|
+
to: public_destination(transition_definition[:to])
|
|
113
|
+
}.freeze
|
|
114
|
+
|
|
158
115
|
->(machine) {
|
|
159
|
-
|
|
160
|
-
|
|
116
|
+
matched =
|
|
117
|
+
guard.nil? ||
|
|
118
|
+
call_with_optional_event(
|
|
119
|
+
guard,
|
|
120
|
+
machine.context,
|
|
121
|
+
machine.current_event
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if matched
|
|
125
|
+
machine.selected_transition_action = action
|
|
126
|
+
machine.selected_transition_metadata = metadata
|
|
127
|
+
end
|
|
128
|
+
matched
|
|
161
129
|
}
|
|
162
130
|
end
|
|
163
131
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
132
|
+
def build_transition_action_callback
|
|
133
|
+
->(machine) {
|
|
134
|
+
callable = machine.selected_transition_action
|
|
135
|
+
unless callable.nil?
|
|
136
|
+
metadata = machine.selected_transition_metadata || {}
|
|
137
|
+
result = call_with_optional_event(
|
|
138
|
+
callable,
|
|
139
|
+
machine.context,
|
|
140
|
+
machine.current_event
|
|
141
|
+
)
|
|
142
|
+
if result.is_a?(Phronomy::Task)
|
|
143
|
+
raise Phronomy::InvalidAsyncTransitionActionError,
|
|
144
|
+
transition_task_error_message(metadata)
|
|
145
|
+
end
|
|
146
|
+
machine.context = result if workflow_context_result?(result)
|
|
147
|
+
end
|
|
148
|
+
}
|
|
181
149
|
end
|
|
182
150
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# @param result [Phronomy::Task]
|
|
192
|
-
# @param state_name [Symbol]
|
|
193
|
-
# @param timeout_secs [Numeric, nil]
|
|
194
|
-
# @api private
|
|
195
|
-
def dispatch_task_in_event_loop(machine, result, state_name, timeout_secs)
|
|
196
|
-
machine.async_pending = true
|
|
197
|
-
session_id = machine.session_id || machine.context.thread_id
|
|
198
|
-
if timeout_secs
|
|
199
|
-
machine.timer_queue_provider.call.schedule(seconds: timeout_secs) do
|
|
200
|
-
next if result.done?
|
|
201
|
-
|
|
202
|
-
machine.event_loop.post(
|
|
203
|
-
Phronomy::Event.new(
|
|
204
|
-
type: :error,
|
|
205
|
-
target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
|
|
206
|
-
payload: {session_id: session_id, result: Phronomy::ActionTimeoutError.new(
|
|
207
|
-
"Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
|
|
208
|
-
)}
|
|
209
|
-
)
|
|
210
|
-
)
|
|
151
|
+
def build_entry_callback(callable, state_name)
|
|
152
|
+
->(machine) {
|
|
153
|
+
result = callable.call(machine.context)
|
|
154
|
+
if result.is_a?(Phronomy::Task)
|
|
155
|
+
raise Phronomy::InvalidAsyncEntryActionError,
|
|
156
|
+
"Entry action for state #{state_name.inspect} returned Phronomy::Task. " \
|
|
157
|
+
"Start the asynchronous operation, register its callback/listener, " \
|
|
158
|
+
"and return the WorkflowContext or nil."
|
|
211
159
|
end
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
160
|
+
machine.context = result if workflow_context_result?(result)
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def build_exit_callback(callable, state_name)
|
|
165
|
+
->(machine) {
|
|
166
|
+
result = callable.call(machine.context)
|
|
167
|
+
if result.is_a?(Phronomy::Task)
|
|
168
|
+
raise Phronomy::InvalidAsyncEntryActionError,
|
|
169
|
+
"Exit action for state #{state_name.inspect} returned Phronomy::Task. " \
|
|
170
|
+
"Exit actions are synchronous Run-to-Completion callbacks."
|
|
219
171
|
end
|
|
220
|
-
|
|
221
|
-
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def call_with_optional_event(callable, context, event)
|
|
176
|
+
parameters =
|
|
177
|
+
if callable.respond_to?(:parameters)
|
|
178
|
+
callable.parameters
|
|
222
179
|
else
|
|
223
|
-
|
|
180
|
+
callable.method(:call).parameters
|
|
224
181
|
end
|
|
225
|
-
|
|
226
|
-
|
|
182
|
+
accepts_event = parameters.length >= 2
|
|
183
|
+
accepts_event ? callable.call(context, event) : callable.call(context)
|
|
227
184
|
end
|
|
228
185
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# Blocks the current execution context until the task completes or the
|
|
232
|
-
# optional timeout elapses.
|
|
233
|
-
#
|
|
234
|
-
# @param machine [Object] phase machine instance
|
|
235
|
-
# @param result [Phronomy::Task]
|
|
236
|
-
# @param state_name [Symbol]
|
|
237
|
-
# @param timeout_secs [Numeric, nil]
|
|
238
|
-
# @api private
|
|
239
|
-
def await_task_blocking(machine, result, state_name, timeout_secs)
|
|
240
|
-
enforce_timeout!(result, state_name, timeout_secs)
|
|
241
|
-
task_result = result.wait_result
|
|
242
|
-
machine.context = task_result if task_result.is_a?(Phronomy::WorkflowContext)
|
|
186
|
+
def workflow_context_result?(result)
|
|
187
|
+
result.respond_to?(:set_graph_metadata)
|
|
243
188
|
end
|
|
244
189
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
# @param timeout_secs [Numeric, nil]
|
|
251
|
-
# @api private
|
|
252
|
-
def enforce_timeout!(result, state_name, timeout_secs)
|
|
253
|
-
return unless timeout_secs
|
|
254
|
-
return unless result.join(timeout_secs).nil?
|
|
190
|
+
def public_destination(destination)
|
|
191
|
+
return :__finish__ if destination == Phronomy::WorkflowRunner::FINISH
|
|
192
|
+
|
|
193
|
+
destination
|
|
194
|
+
end
|
|
255
195
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"
|
|
196
|
+
def transition_task_error_message(metadata)
|
|
197
|
+
"Transition action " \
|
|
198
|
+
"#{metadata[:from].inspect} --#{metadata[:event].inspect}--> " \
|
|
199
|
+
"#{metadata[:to].inspect} returned Phronomy::Task. " \
|
|
200
|
+
"Start the asynchronous operation, register its callback/listener, " \
|
|
201
|
+
"and return the WorkflowContext or nil."
|
|
259
202
|
end
|
|
260
203
|
end
|
|
261
204
|
end
|