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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +236 -57
  4. data/benchmark/bench_agent_invoke.rb +2 -3
  5. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
  6. data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
  7. data/examples/workflows/agent_event_mapping.rb +104 -0
  8. data/examples/workflows/generic_task_event_mapping.rb +58 -0
  9. data/lib/phronomy/agent/agent_invocation.rb +385 -0
  10. data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
  11. data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
  12. data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
  13. data/lib/phronomy/agent/async_event_api.rb +471 -0
  14. data/lib/phronomy/agent/base.rb +500 -411
  15. data/lib/phronomy/agent/context/capability/base.rb +51 -119
  16. data/lib/phronomy/agent/llm_operation_result.rb +23 -0
  17. data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
  18. data/lib/phronomy/agent/tool_approval_request.rb +121 -0
  19. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
  20. data/lib/phronomy/agent/tool_executor.rb +47 -69
  21. data/lib/phronomy/agent/tool_invocation.rb +634 -0
  22. data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
  23. data/lib/phronomy/agent.rb +21 -9
  24. data/lib/phronomy/configuration.rb +42 -6
  25. data/lib/phronomy/engine/event_loop.rb +269 -112
  26. data/lib/phronomy/engine/fsm_session.rb +180 -142
  27. data/lib/phronomy/engine/task.rb +5 -10
  28. data/lib/phronomy/event.rb +8 -8
  29. data/lib/phronomy/generator_verifier.rb +253 -142
  30. data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
  31. data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
  32. data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
  33. data/lib/phronomy/invocation_context.rb +5 -19
  34. data/lib/phronomy/llm_adapter/base.rb +25 -34
  35. data/lib/phronomy/metrics.rb +2 -0
  36. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
  37. data/lib/phronomy/stream_callback_error.rb +35 -0
  38. data/lib/phronomy/tools/mcp.rb +25 -0
  39. data/lib/phronomy/version.rb +1 -1
  40. data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
  41. data/lib/phronomy/workflow.rb +122 -261
  42. data/lib/phronomy/workflow_context.rb +54 -102
  43. data/lib/phronomy/workflow_runner.rb +238 -300
  44. data/lib/phronomy.rb +6 -4
  45. data/scripts/check_readme_runnable.rb +4 -1
  46. metadata +18 -7
  47. data/lib/phronomy/agent/concerns/retryable.rb +0 -103
  48. data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
  49. data/lib/phronomy/agent/invocation_context.rb +0 -171
  50. data/lib/phronomy/agent/invocation_session.rb +0 -352
  51. data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-27 00:00:00.000000000 Z
11
+ date: 2026-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -155,30 +155,37 @@ files:
155
155
  - docs/decisions/009-state-store-abstraction.md
156
156
  - docs/decisions/010-cooperative-first-concurrency.md
157
157
  - docs/decisions/011-build-context-as-single-llm-input-authority.md
158
+ - docs/decisions/011-delegate-transport-policy-to-adapters.md
158
159
  - docs/mcp-client.md
160
+ - examples/workflows/agent_event_mapping.rb
161
+ - examples/workflows/generic_task_event_mapping.rb
159
162
  - gemfiles/mcp_1_0.gemfile
160
163
  - lib/phronomy.rb
161
164
  - lib/phronomy/agent.rb
165
+ - lib/phronomy/agent/agent_invocation.rb
166
+ - lib/phronomy/agent/agent_invocation_registry.rb
167
+ - lib/phronomy/agent/agent_invocation_session_builder.rb
168
+ - lib/phronomy/agent/approval_evaluation_request.rb
169
+ - lib/phronomy/agent/async_event_api.rb
162
170
  - lib/phronomy/agent/base.rb
163
171
  - lib/phronomy/agent/before_completion_context.rb
164
172
  - lib/phronomy/agent/concerns/before_completion.rb
165
173
  - lib/phronomy/agent/concerns/error_translation.rb
166
174
  - lib/phronomy/agent/concerns/filterable.rb
167
- - lib/phronomy/agent/concerns/retryable.rb
168
175
  - lib/phronomy/agent/context/capability/base.rb
169
- - lib/phronomy/agent/context/capability/scope_policy.rb
170
176
  - lib/phronomy/agent/context/instruction/prompt_template.rb
171
177
  - lib/phronomy/agent/context/knowledge/base.rb
172
178
  - lib/phronomy/agent/context/knowledge/entity_knowledge.rb
173
179
  - lib/phronomy/agent/context/knowledge/static_knowledge.rb
174
- - lib/phronomy/agent/invocation_context.rb
175
- - lib/phronomy/agent/invocation_session.rb
180
+ - lib/phronomy/agent/llm_operation_result.rb
176
181
  - lib/phronomy/agent/phase_machine_builder.rb
177
182
  - lib/phronomy/agent/runner.rb
178
183
  - lib/phronomy/agent/shared_state.rb
179
- - lib/phronomy/agent/suspended_session_registry.rb
184
+ - lib/phronomy/agent/tool_approval_request.rb
180
185
  - lib/phronomy/agent/tool_call_intercepted.rb
181
186
  - lib/phronomy/agent/tool_executor.rb
187
+ - lib/phronomy/agent/tool_invocation.rb
188
+ - lib/phronomy/agent/tool_invocation_session_builder.rb
182
189
  - lib/phronomy/configuration.rb
183
190
  - lib/phronomy/diagnostics.rb
184
191
  - lib/phronomy/engine/concurrency/async_queue.rb
@@ -225,6 +232,9 @@ files:
225
232
  - lib/phronomy/filter/base.rb
226
233
  - lib/phronomy/filter/prompt_injection_filter.rb
227
234
  - lib/phronomy/generator_verifier.rb
235
+ - lib/phronomy/invalid_async_entry_action_error.rb
236
+ - lib/phronomy/invalid_async_transition_action_error.rb
237
+ - lib/phronomy/invalid_async_workflow_action_error.rb
228
238
  - lib/phronomy/invocation_context.rb
229
239
  - lib/phronomy/knowledge_source.rb
230
240
  - lib/phronomy/llm_adapter.rb
@@ -247,6 +257,7 @@ files:
247
257
  - lib/phronomy/runnable.rb
248
258
  - lib/phronomy/state_store/base.rb
249
259
  - lib/phronomy/state_store/in_memory.rb
260
+ - lib/phronomy/stream_callback_error.rb
250
261
  - lib/phronomy/testing.rb
251
262
  - lib/phronomy/testing/fake_clock.rb
252
263
  - lib/phronomy/testing/fake_scheduler.rb
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Concerns
6
- # Adds configurable retry behaviour to an agent.
7
- #
8
- # Included in {Phronomy::Agent::Base}. The retry loop wraps the full
9
- # #invoke_once call; {Phronomy::FilterBlockError} is never retried.
10
- # @api private
11
- module Retryable
12
- def self.included(base)
13
- base.extend(ClassMethods)
14
- end
15
-
16
- # Class-level DSL methods mixed into the including agent class.
17
- module ClassMethods
18
- # Configures a retry policy that wraps the full #invoke call.
19
- # FilterBlockError is never retried regardless of this setting.
20
- #
21
- # @param times [Integer] maximum retry attempts (default: 0)
22
- # @param wait [Symbol, Numeric] :exponential, :linear, or a fixed Float
23
- # @param base [Float] base wait time in seconds (default: 1.0)
24
- #
25
- # @example
26
- # class MyAgent < Phronomy::Agent::Base
27
- # retry_policy times: 2, wait: :exponential, base: 1.0
28
- # end
29
- # @api private
30
- def retry_policy(times: 0, wait: 0, base: 1.0)
31
- @_retry_policy = {times: times, wait: wait, base: base}
32
- end
33
-
34
- # Returns the configured retry policy, or nil when none is set.
35
- # @return [Hash, nil]
36
- attr_reader :_retry_policy
37
-
38
- # Injectable sleep callable for testing.
39
- # In the EventLoop-driven invoke path, actual delay is handled by
40
- # timer_queue (non-blocking); _sleep_proc is called only as an
41
- # instrumentation hook. The default is a no-op; tests override it
42
- # with a spy lambda to record sleep durations.
43
- # @return [#call]
44
- # @api private
45
- def _sleep_proc
46
- @_sleep_proc || method(:sleep)
47
- end
48
-
49
- # Overrides the sleep callable used between retries.
50
- # @param proc [#call]
51
- attr_writer :_sleep_proc
52
- end
53
-
54
- private
55
-
56
- # Retry loop for #invoke.
57
- def _invoke_impl(input, messages: [], thread_id: nil, config: {})
58
- # Fail fast when the token is already cancelled before any LLM call.
59
- if (token = config[:cancellation_token]) && token.cancelled?
60
- raise Phronomy::CancellationError, "invocation cancelled"
61
- end
62
-
63
- policy = self.class._retry_policy
64
- attempt = 0
65
- begin
66
- _invoke_via_fsm(input, messages: messages, thread_id: thread_id, config: config)
67
- rescue Phronomy::FilterBlockError
68
- raise
69
- rescue Phronomy::CancellationError
70
- raise # Never retry after cancellation.
71
- rescue
72
- if policy && attempt < policy[:times]
73
- wait = compute_agent_retry_wait(policy[:wait], policy[:base], attempt)
74
- self.class._sleep_proc.call(wait) if wait > 0
75
- attempt += 1
76
- retry
77
- end
78
- translate_and_reraise!($!)
79
- end
80
- end
81
-
82
- # Computes the agent-level retry wait duration.
83
- # @param strategy [Symbol, Numeric]
84
- # @param base [Float]
85
- # @param attempt [Integer]
86
- # @return [Float]
87
- # @api private
88
- def compute_agent_retry_wait(strategy, base, attempt)
89
- case strategy
90
- when :exponential
91
- (2**attempt) * base
92
- when :linear
93
- (attempt + 1) * base
94
- when Numeric
95
- strategy.to_f
96
- else
97
- base.to_f
98
- end
99
- end
100
- end
101
- end
102
- end
103
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Capability
7
- # Evaluates whether a tool with a given scope may execute.
8
- #
9
- # A ScopePolicy is a callable that receives +(tool_class, scope, agent)+ and
10
- # returns one of:
11
- # +:allow+ — proceed immediately without an approval gate.
12
- # +:reject+ — block execution; the tool returns a denial message.
13
- # +:approve+ — delegate to the agent's approval handler (if registered);
14
- # when no handler is registered the call is rejected.
15
- #
16
- # The {Default} instance is used automatically when no custom policy is
17
- # configured on an agent.
18
- #
19
- # @example Custom policy that allows everything
20
- # agent.scope_policy = ->(_tool_class, _scope, _agent) { :allow }
21
- #
22
- # @example Strict policy that rejects all write scopes
23
- # agent.scope_policy = ->(_tc, scope, _agent) {
24
- # scope == :write ? :reject : :allow
25
- # }
26
- class ScopePolicy
27
- # Scopes that must go through an approval gate before execution.
28
- APPROVAL_REQUIRED_SCOPES = %i[write admin external_network filesystem process external_process].freeze
29
-
30
- # Scopes that are always permitted without approval.
31
- ALWAYS_ALLOWED_SCOPES = %i[read_only].freeze
32
-
33
- # Returns +:allow+ for always-allowed scopes, +:approve+ for high-risk
34
- # scopes, and +:allow+ for anything else (including +nil+).
35
- #
36
- # @param _tool_class [Class]
37
- # @param scope [Symbol, nil]
38
- # @param _agent [Object]
39
- # @return [:allow, :approve, :reject]
40
- # @api private
41
- def call(_tool_class, scope, _agent)
42
- return :allow if scope.nil? || ALWAYS_ALLOWED_SCOPES.include?(scope)
43
- return :approve if APPROVAL_REQUIRED_SCOPES.include?(scope)
44
-
45
- :allow
46
- end
47
-
48
- # Shared singleton used when no custom policy is configured.
49
- DEFAULT = new.freeze
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,171 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- # Holds the mutable state for a single Agent#invoke execution.
6
- #
7
- # An InvocationContext is created at the start of each invoke call and
8
- # passed through every FSM state as the context object. It plays the same
9
- # role as WorkflowContext does for Workflow executions.
10
- #
11
- # Fields:
12
- # input — the (possibly filter-transformed) user input
13
- # messages — conversation history (Array of RubyLLM::Message)
14
- # chat — the RubyLLM::Chat object built during :building_context
15
- # output — the final LLM text response
16
- # usage — Phronomy::TokenUsage after completion
17
- # tool_call_pending — whether the last LLM response contained a tool call
18
- # approval_required — whether the pending tool requires human approval
19
- # input_blocked — whether an input filter called block!
20
- # output_blocked — whether an output filter called block!
21
- # block_error — the FilterBlockError raised (for :blocked terminal)
22
- # pending_tool_call — the intercepted tool_call object (name/arguments/id)
23
- # user_message_sent — true after the first LLM call (continuation uses nil msg)
24
- # sync_approval_handler — true when agent has an on_approval_required block
25
- # session_id — FSM session id (set by set_graph_metadata)
26
- # phase — terminal FSM phase (set by set_graph_metadata)
27
- #
28
- # @api private
29
- class InvocationContext
30
- # @return [String, Hash] user input (may be transformed by input filters)
31
- attr_accessor :input
32
-
33
- # @return [Array] conversation history
34
- attr_accessor :messages
35
-
36
- # @return [Object, nil] RubyLLM::Chat instance
37
- attr_accessor :chat
38
-
39
- # @return [String, nil] final LLM output text
40
- attr_accessor :output
41
-
42
- # @return [Phronomy::TokenUsage, nil]
43
- attr_accessor :usage
44
-
45
- # @return [Boolean] true when calling_llm returned a tool call
46
- attr_accessor :tool_call_pending
47
-
48
- # @return [Boolean] true when the pending tool requires human approval
49
- attr_accessor :approval_required
50
-
51
- # @return [Boolean] true when an input filter called block!
52
- attr_accessor :input_blocked
53
-
54
- # @return [Boolean] true when an output filter called block!
55
- attr_accessor :output_blocked
56
-
57
- # @return [Phronomy::FilterBlockError, nil]
58
- attr_accessor :block_error
59
-
60
- # @return [Object, nil] intercepted RubyLLM tool_call (name/arguments/id)
61
- attr_accessor :pending_tool_call
62
-
63
- # @return [Boolean] true after the first LLM call (continuation passes nil msg)
64
- attr_accessor :user_message_sent
65
-
66
- # @return [Boolean] true when Agent.approve was called (signals executing_tool
67
- # to execute the pending tool rather than re-suspending)
68
- attr_accessor :approved
69
-
70
- # @return [Boolean] true when the pending tool call was rejected via
71
- # Agent.approve(session_id, approved: false)
72
- attr_accessor :rejected
73
-
74
- # @return [String, nil] FSM session id (set by set_graph_metadata)
75
- attr_reader :session_id
76
-
77
- # @return [Symbol, nil] terminal FSM phase (set by set_graph_metadata)
78
- attr_reader :phase
79
-
80
- # @return [Boolean] true when agent has a synchronous on_approval_required handler
81
- attr_reader :sync_approval_handler
82
-
83
- # @return [Phronomy::Agent::Base] the agent instance driving this invocation
84
- attr_reader :agent
85
-
86
- # @return [Hash] the config hash passed to invoke
87
- attr_reader :config
88
-
89
- # @return [String, nil] thread_id from config
90
- attr_reader :thread_id
91
-
92
- # @param agent [Phronomy::Agent::Base]
93
- # @param input [String, Hash]
94
- # @param messages [Array]
95
- # @param config [Hash]
96
- # @api private
97
- def initialize(agent:, input:, messages:, config:)
98
- @agent = agent
99
- @input = input
100
- @messages = Array(messages)
101
- @config = config
102
- @thread_id = config[:thread_id]
103
- @chat = nil
104
- @output = nil
105
- @usage = nil
106
- @tool_call_pending = false
107
- @approval_required = false
108
- @input_blocked = false
109
- @output_blocked = false
110
- @block_error = nil
111
- @pending_tool_call = nil
112
- @user_message_sent = false
113
- @approved = false
114
- @rejected = false
115
- @sync_approval_handler = !agent.instance_variable_get(:@approval_handler).nil?
116
- @session_id = nil
117
- @phase = nil
118
- end
119
-
120
- # Called by FSMSession#finish! and FSMSession#halt! to record the
121
- # terminal/halted phase and the session id.
122
- # Mirrors the WorkflowContext interface so FSMSession works with both.
123
- # @param thread_id [String, nil]
124
- # @param phase [Symbol, nil]
125
- # @return [void]
126
- # @api private
127
- def set_graph_metadata(thread_id: nil, phase: nil)
128
- @session_id = thread_id if thread_id
129
- @phase = phase
130
- end
131
-
132
- # Guard helpers used by PhaseMachineBuilder transitions.
133
-
134
- # @return [Boolean]
135
- # @api private
136
- def input_passed?
137
- !@input_blocked
138
- end
139
-
140
- # @return [Boolean]
141
- # @api private
142
- def input_blocked?
143
- @input_blocked
144
- end
145
-
146
- # @return [Boolean]
147
- # @api private
148
- def output_passed?
149
- !@output_blocked
150
- end
151
-
152
- # @return [Boolean]
153
- # @api private
154
- def output_blocked?
155
- @output_blocked
156
- end
157
-
158
- # @return [Boolean]
159
- # @api private
160
- def tool_call_pending?
161
- @tool_call_pending
162
- end
163
-
164
- # @return [Boolean]
165
- # @api private
166
- def approval_required?
167
- @approval_required
168
- end
169
- end
170
- end
171
- end