phronomy 0.13.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 +155 -0
- data/README.md +266 -38
- 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/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -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 +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- 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 +58 -53
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- 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 +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- 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 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
# Module for defining
|
|
5
|
-
# Include in a class and use the field DSL to declare context fields.
|
|
4
|
+
# Module for defining Workflow context data.
|
|
6
5
|
#
|
|
7
|
-
# In StateChart terminology this is the
|
|
8
|
-
#
|
|
9
|
-
# directly, as opposed to the current phase (which is the machine's state).
|
|
6
|
+
# In StateChart terminology this is the extended state/context, as opposed to
|
|
7
|
+
# the current FSM phase.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# class ScanContext
|
|
18
|
-
# include Phronomy::WorkflowContext
|
|
19
|
-
# field :messages, type: :append, default: -> { [] }
|
|
20
|
-
# field :query, type: :replace
|
|
21
|
-
# field :metadata, type: :merge, default: -> { {} }
|
|
22
|
-
# end
|
|
9
|
+
# An application context may define +handle_fsm_event(event)+. FSMSession calls
|
|
10
|
+
# it on the EventLoop thread before evaluating a declared transition. The
|
|
11
|
+
# method may mutate the context and return +false+ to continue transition
|
|
12
|
+
# evaluation, return a replacement WorkflowContext, or return +:consume+ to
|
|
13
|
+
# discard a stale/unrelated event without firing a transition.
|
|
23
14
|
module WorkflowContext
|
|
24
15
|
def self.included(base)
|
|
25
16
|
base.extend(ClassMethods)
|
|
@@ -27,12 +18,6 @@ module Phronomy
|
|
|
27
18
|
end
|
|
28
19
|
|
|
29
20
|
module ClassMethods
|
|
30
|
-
# Defines a context field.
|
|
31
|
-
# @param name [Symbol]
|
|
32
|
-
# @param type [Symbol] :replace / :append / :merge
|
|
33
|
-
# @param default [Object, Proc, nil]
|
|
34
|
-
# @raise [ArgumentError] if +default+ is a plain Array or Hash (use a Proc instead)
|
|
35
|
-
# @api public
|
|
36
21
|
def field(name, type: :replace, default: nil)
|
|
37
22
|
if default.is_a?(Array) || default.is_a?(Hash)
|
|
38
23
|
raise ArgumentError,
|
|
@@ -42,12 +27,8 @@ module Phronomy
|
|
|
42
27
|
end
|
|
43
28
|
|
|
44
29
|
@fields[name] = {type: type, default: default}
|
|
45
|
-
|
|
46
|
-
# Define getter.
|
|
47
30
|
attr_reader name
|
|
48
31
|
|
|
49
|
-
# Define write-guarded setter. Mutation from outside the EventLoop
|
|
50
|
-
# dispatch thread raises WorkflowContextOwnershipError in EventLoop mode.
|
|
51
32
|
define_method(:"#{name}=") do |value|
|
|
52
33
|
_assert_write_permitted!
|
|
53
34
|
instance_variable_set(:"@#{name}", value)
|
|
@@ -59,86 +40,71 @@ module Phronomy
|
|
|
59
40
|
end
|
|
60
41
|
end
|
|
61
42
|
|
|
62
|
-
# Internal workflow metadata accessors (not user-defined fields).
|
|
63
|
-
# These are preserved through merge but excluded from to_h.
|
|
64
43
|
attr_reader :thread_id
|
|
65
44
|
|
|
66
|
-
# Returns the current execution phase of the workflow.
|
|
67
|
-
# Encoding:
|
|
68
|
-
# :__end__ — workflow completed (or not yet started)
|
|
69
|
-
# :awaiting_<name> — halted at a wait_state(:awaiting_<name>) declaration
|
|
70
|
-
# :<state> — resuming at <state> (workflow paused before its execution)
|
|
71
|
-
# @return [Symbol]
|
|
72
|
-
# @api public
|
|
73
|
-
# mutant:disable - @phase is always non-nil (set to :__end__ in initialize, only changed by set_graph_metadata which never sets nil), so the || :__end__ fallback branch is never reached — all mutations of the right-hand side are genuine equivalents
|
|
74
45
|
def phase
|
|
75
46
|
@phase || :__end__
|
|
76
47
|
end
|
|
77
48
|
|
|
78
|
-
# Returns true if the workflow is paused mid-execution (not yet completed).
|
|
79
|
-
# @return [Boolean]
|
|
80
|
-
# @api public
|
|
81
|
-
# mutant:disable - phase != :__end__ vs !phase.eql?(:__end__) vs !phase.equal?(:__end__) are genuine equivalents for Symbol (Symbols are interned so == / eql? / equal? all behave identically)
|
|
82
49
|
def halted?
|
|
83
50
|
phase != :__end__
|
|
84
51
|
end
|
|
85
52
|
|
|
86
|
-
# Sets internal workflow metadata. Returns self.
|
|
87
|
-
# @param thread_id [String, nil]
|
|
88
|
-
# @param phase [Symbol, nil]
|
|
89
|
-
# @api public
|
|
90
|
-
# mutant:disable - mutations replacing return value `self` with nil or removing the last line are genuine equivalents: callers chain on the return value only in merge which immediately discards it
|
|
91
53
|
def set_graph_metadata(thread_id: nil, phase: nil)
|
|
92
54
|
@thread_id = thread_id unless thread_id.nil?
|
|
93
55
|
@phase = phase unless phase.nil?
|
|
94
56
|
self
|
|
95
57
|
end
|
|
96
58
|
|
|
97
|
-
# mutant:disable - multiple genuine equivalent mutations: is_a?(Proc) vs instance_of?(Proc) (Proc has no subclasses in practice), config[]/fetch() for always-present :default key, @thread_id=nil removal (unset ivar is already nil), @phase=:__end__ → nil or removal (phase method returns :__end__ via @phase||:__end__ fallback), raise message #{.inspect} vs #{} (spec checks exception class not message text)
|
|
98
59
|
def initialize(**attrs)
|
|
99
60
|
unknown = attrs.keys - self.class.fields.keys
|
|
100
|
-
|
|
61
|
+
unless unknown.empty?
|
|
62
|
+
raise ArgumentError,
|
|
63
|
+
"Unknown WorkflowContext field(s): #{unknown.inspect}"
|
|
64
|
+
end
|
|
101
65
|
|
|
102
66
|
self.class.fields.each do |name, config|
|
|
103
|
-
default =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
67
|
+
default =
|
|
68
|
+
if config[:default].is_a?(Proc)
|
|
69
|
+
config[:default].call
|
|
70
|
+
else
|
|
71
|
+
config[:default]
|
|
72
|
+
end
|
|
73
|
+
instance_variable_set(
|
|
74
|
+
:"@#{name}",
|
|
75
|
+
attrs.fetch(name, default)
|
|
76
|
+
)
|
|
107
77
|
end
|
|
108
78
|
@thread_id = nil
|
|
109
79
|
@phase = :__end__
|
|
110
80
|
end
|
|
111
81
|
|
|
112
|
-
# Returns a new context
|
|
113
|
-
# Updated fields follow the field's declared +:type+ semantics (:replace, :append,
|
|
114
|
-
# or :merge). Unchanged fields are deep-copied on a best-effort basis — objects
|
|
115
|
-
# that do not support +#dup+ (e.g. integers, frozen objects) are carried over
|
|
116
|
-
# by reference. Internal workflow metadata (thread_id, phase) is preserved.
|
|
117
|
-
# @param updates [Hash] { field_name => new_value }
|
|
118
|
-
# @return [self.class] new context instance
|
|
119
|
-
# @raise [ArgumentError] if updates contains keys that are not declared fields
|
|
120
|
-
# @api public
|
|
121
|
-
# mutant:disable - multiple genuine equivalent mutations: send/public_send/__send__ are identical (all field accessors are public), fields[]/fetch() and field_config[]/fetch() for always-present keys, updates[]/fetch() when updates.key?(name) is already true, Array() wrapping for append fields that always hold Arrays, (send||{})/send equivalence for merge fields that always hold Hashes, deep_dup_value(send) vs send are equivalent under killfork (coverage selection does not trace the deep_dup_value call site across the fork boundary), raise message inspect vs to_s (spec checks exception class only)
|
|
82
|
+
# Returns a new context with field update policies applied.
|
|
122
83
|
def merge(updates)
|
|
123
84
|
unknown = updates.keys - self.class.fields.keys
|
|
124
|
-
|
|
85
|
+
unless unknown.empty?
|
|
86
|
+
raise ArgumentError,
|
|
87
|
+
"Unknown WorkflowContext field(s): #{unknown.inspect}"
|
|
88
|
+
end
|
|
125
89
|
|
|
126
90
|
new_attrs = {}
|
|
127
91
|
self.class.fields.each_key do |name|
|
|
128
92
|
field_config = self.class.fields[name]
|
|
129
|
-
new_attrs[name] =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
93
|
+
new_attrs[name] =
|
|
94
|
+
if updates.key?(name)
|
|
95
|
+
case field_config[:type]
|
|
96
|
+
when :append
|
|
97
|
+
Array(public_send(name)) + Array(updates[name])
|
|
98
|
+
when :merge
|
|
99
|
+
(public_send(name) || {}).merge(updates[name])
|
|
100
|
+
else
|
|
101
|
+
updates[name]
|
|
102
|
+
end
|
|
135
103
|
else
|
|
136
|
-
|
|
104
|
+
deep_dup_value(public_send(name))
|
|
137
105
|
end
|
|
138
|
-
else
|
|
139
|
-
deep_dup_value(send(name))
|
|
140
|
-
end
|
|
141
106
|
end
|
|
107
|
+
|
|
142
108
|
new_context = self.class.new(**new_attrs)
|
|
143
109
|
new_context.set_graph_metadata(
|
|
144
110
|
thread_id: @thread_id,
|
|
@@ -147,30 +113,19 @@ module Phronomy
|
|
|
147
113
|
new_context
|
|
148
114
|
end
|
|
149
115
|
|
|
150
|
-
# Converts user-defined fields to a Hash (excludes internal workflow metadata).
|
|
151
|
-
# @return [Hash]
|
|
152
|
-
# @api public
|
|
153
|
-
# mutant:disable - send/public_send/__send__ are genuine equivalents (all field accessors are public methods)
|
|
154
116
|
def to_h
|
|
155
|
-
self.class.fields.keys.each_with_object({}) do |name,
|
|
156
|
-
|
|
117
|
+
self.class.fields.keys.each_with_object({}) do |name, result|
|
|
118
|
+
result[name] = public_send(name)
|
|
157
119
|
end
|
|
158
120
|
end
|
|
159
121
|
|
|
160
122
|
private
|
|
161
123
|
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# (e.g. Workflow#stream using the run_workflow sync path).
|
|
166
|
-
# @raise [Phronomy::WorkflowContextOwnershipError]
|
|
167
|
-
# @api private
|
|
168
|
-
# mutant:disable - multiple genuine equivalent mutations: defined?(Phronomy::EventLoop)&& removal is genuine because EventLoop is always loaded in the killfork environment; true&& is genuine (truthy guard); EventLoop.current? resolves to Phronomy::EventLoop.current? within the Phronomy module; WorkflowContextOwnershipError resolves to Phronomy::WorkflowContextOwnershipError within the module; raise without message or with nil message is genuine (spec checks exception class, not message text)
|
|
124
|
+
# Workflow field mutation is permitted only on the Runtime-owned EventLoop
|
|
125
|
+
# dispatch thread. All Workflow execution APIs now use that same path; there
|
|
126
|
+
# is no caller-thread synchronous exception.
|
|
169
127
|
def _assert_write_permitted!
|
|
170
|
-
return
|
|
171
|
-
# Allow mutations when executing synchronously (e.g. Workflow#stream via run_workflow).
|
|
172
|
-
return if Thread.current[:phronomy_sync_execution]
|
|
173
|
-
return if Phronomy::EventLoop.current?
|
|
128
|
+
return if Phronomy::Runtime.in_event_loop_context?
|
|
174
129
|
|
|
175
130
|
raise Phronomy::WorkflowContextOwnershipError,
|
|
176
131
|
"WorkflowContext fields may only be mutated from the EventLoop dispatch " \
|
|
@@ -178,27 +133,23 @@ module Phronomy
|
|
|
178
133
|
"updates as event payloads."
|
|
179
134
|
end
|
|
180
135
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# Immutable values (nil, Symbol, Integer, Float, true/false, frozen String) are returned as-is.
|
|
184
|
-
# Other objects are dup'd (best-effort shallow copy for custom types).
|
|
185
|
-
# Objects that cannot be dup'd (e.g. Proc, Method) are returned as-is.
|
|
186
|
-
# mutant:disable - multiple genuine equivalent mutations: each class in the when clause (NilClass/Symbol/Integer/Float/TrueClass/FalseClass) can be removed or replaced with nil because all those types are frozen so the else-branch val.frozen? guard returns the same result; return val vs val is also equivalent; if val.frozen? vs if self.frozen? is equivalent since self is never frozen in this context
|
|
187
|
-
def deep_dup_value(val)
|
|
188
|
-
case val
|
|
136
|
+
def deep_dup_value(value)
|
|
137
|
+
case value
|
|
189
138
|
when Array
|
|
190
|
-
|
|
139
|
+
value.map { |item| deep_dup_value(item) }
|
|
191
140
|
when Hash
|
|
192
|
-
|
|
141
|
+
value.each_with_object({}) do |(key, item), result|
|
|
142
|
+
result[key] = deep_dup_value(item)
|
|
143
|
+
end
|
|
193
144
|
when NilClass, Symbol, Integer, Float, TrueClass, FalseClass
|
|
194
|
-
|
|
145
|
+
value
|
|
195
146
|
else
|
|
196
|
-
return
|
|
147
|
+
return value if value.frozen?
|
|
197
148
|
|
|
198
149
|
begin
|
|
199
|
-
|
|
150
|
+
value.dup
|
|
200
151
|
rescue TypeError
|
|
201
|
-
|
|
152
|
+
value
|
|
202
153
|
end
|
|
203
154
|
end
|
|
204
155
|
end
|