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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/README.md +266 -38
  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/docs/mcp-client.md +75 -0
  8. data/examples/workflows/agent_event_mapping.rb +104 -0
  9. data/examples/workflows/generic_task_event_mapping.rb +58 -0
  10. data/gemfiles/mcp_1_0.gemfile +9 -0
  11. data/lib/phronomy/agent/agent_invocation.rb +385 -0
  12. data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
  13. data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
  14. data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
  15. data/lib/phronomy/agent/async_event_api.rb +471 -0
  16. data/lib/phronomy/agent/base.rb +509 -420
  17. data/lib/phronomy/agent/context/capability/base.rb +57 -119
  18. data/lib/phronomy/agent/llm_operation_result.rb +23 -0
  19. data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
  20. data/lib/phronomy/agent/tool_approval_request.rb +121 -0
  21. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
  22. data/lib/phronomy/agent/tool_executor.rb +47 -69
  23. data/lib/phronomy/agent/tool_invocation.rb +634 -0
  24. data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
  25. data/lib/phronomy/agent.rb +21 -9
  26. data/lib/phronomy/configuration.rb +58 -53
  27. data/lib/phronomy/diagnostics.rb +1 -1
  28. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
  29. data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
  30. data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
  31. data/lib/phronomy/engine/event_loop.rb +507 -303
  32. data/lib/phronomy/engine/fsm_session.rb +181 -140
  33. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
  34. data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
  35. data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
  36. data/lib/phronomy/engine/runtime.rb +247 -57
  37. data/lib/phronomy/engine/task.rb +5 -10
  38. data/lib/phronomy/event.rb +8 -8
  39. data/lib/phronomy/generator_verifier.rb +253 -142
  40. data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
  41. data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
  42. data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
  43. data/lib/phronomy/invocation_context.rb +5 -19
  44. data/lib/phronomy/llm_adapter/base.rb +25 -34
  45. data/lib/phronomy/metrics.rb +6 -3
  46. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
  47. data/lib/phronomy/stream_callback_error.rb +35 -0
  48. data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
  49. data/lib/phronomy/tools/mcp.rb +410 -81
  50. data/lib/phronomy/version.rb +1 -1
  51. data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
  52. data/lib/phronomy/workflow.rb +122 -261
  53. data/lib/phronomy/workflow_context.rb +55 -104
  54. data/lib/phronomy/workflow_runner.rb +239 -291
  55. data/lib/phronomy.rb +30 -23
  56. data/scripts/check_readme_runnable.rb +4 -1
  57. metadata +63 -11
  58. data/lib/phronomy/agent/concerns/retryable.rb +0 -103
  59. data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
  60. data/lib/phronomy/agent/invocation_context.rb +0 -171
  61. data/lib/phronomy/agent/invocation_session.rb +0 -346
  62. data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
  63. data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
  64. data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
data/lib/phronomy.rb CHANGED
@@ -12,10 +12,12 @@ loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
12
12
  loader.inflector.inflect("rag" => "RAG")
13
13
  # FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
14
14
  # Phronomy::FSMSession is the top-level cooperative execution engine shared by
15
- # WorkflowRunner and Agent::InvocationSession.
15
+ # WorkflowRunner, AgentInvocationSessionBuilder, and ToolInvocationSessionBuilder.
16
16
  loader.inflector.inflect("fsm_session" => "FSMSession")
17
17
  # LLMAdapter: Zeitwerk would infer "LlmAdapter" — override to "LLMAdapter".
18
18
  loader.inflector.inflect("llm_adapter" => "LLMAdapter")
19
+ # LLMOperationResult: preserve the LLM acronym for the Agent result carrier.
20
+ loader.inflector.inflect("llm_operation_result" => "LLMOperationResult")
19
21
  # LLMAdapter::RubyLLM: "ruby_llm" maps to "RubyLLM" (not "RubyLlm").
20
22
  loader.inflector.inflect("ruby_llm" => "RubyLLM")
21
23
  # Collapse engine/ so that its contents autoload directly under Phronomy::
@@ -34,7 +36,7 @@ module Phronomy
34
36
  class ParseError < Error; end
35
37
  class RecursionLimitError < Error; end
36
38
  class ToolError < Error; end
37
- # Raised when an agent invocation exceeds the timeout set via +invoke_timeout+.
39
+ # Base error for Phronomy-owned timed boundaries and generic timeout primitives.
38
40
  class TimeoutError < Error; end
39
41
 
40
42
  class ConfigurationError < Error; end
@@ -77,6 +79,13 @@ module Phronomy
77
79
  # @see Phronomy::Runtime.in_scheduler_context?
78
80
  class SchedulerReentrancyError < Error; end
79
81
 
82
+ # Raised when work is submitted to a Runtime whose shutdown has begun, or
83
+ # when a Runtime cannot be reset because owned resources are still alive.
84
+ class RuntimeShutdownError < Error; end
85
+
86
+ # Raised when Runtime#shutdown is invoked from inside a Phronomy::Task.
87
+ class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
88
+
80
89
  # Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
81
90
  # and the pipeline's combined confidence score falls below the configured threshold.
82
91
  #
@@ -118,8 +127,8 @@ module Phronomy
118
127
  # result is available. Extends {TimeoutError} for backwards compatibility.
119
128
  class ScopeTimeoutError < TimeoutError; end
120
129
 
121
- # Raised when a Workflow entry/exit action task exceeds the +action_timeout:+
122
- # configured for its state. Extends {TimeoutError}.
130
+ # Deprecated compatibility constant. Workflow entry/exit actions are
131
+ # synchronous and the Workflow DSL no longer accepts +action_timeout:+.
123
132
  class ActionTimeoutError < TimeoutError; end
124
133
 
125
134
  # Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
@@ -175,28 +184,26 @@ module Phronomy
175
184
  @configuration = original
176
185
  end
177
186
 
178
- # Resets all Phronomy runtime state: configuration and the EventLoop
179
- # singleton (if running).
187
+ # Shuts down and clears the process-wide default Runtime, then resets
188
+ # global configuration. Intended for test suites only.
180
189
  #
181
- # **Intended for test suites only.** Stops any running EventLoop thread,
182
- # clears the EventLoop singleton, and resets configuration to defaults.
183
- # Call once before/after each example to ensure test isolation.
190
+ # Runtime execution failure and resource cleanup are separate. The
191
+ # singleton is cleared when cleanup completed, even if execution failed.
184
192
  #
185
- # @example
186
- # config.around { |ex| Phronomy.reset_runtime! ; ex.run ; Phronomy.reset_runtime! }
193
+ # @param timeout [Numeric] maximum graceful wait for Runtime tasks and
194
+ # EventLoop shutdown
195
+ # @return [Phronomy::Runtime::ShutdownResult]
187
196
  # @api public
188
- def reset_runtime!
189
- # Do NOT stop the EventLoop here. Since Phase 2, all Agent#invoke calls
190
- # go through FSMSession + EventLoop. Stopping and restarting the EventLoop
191
- # on every after(:each) hook would add thread creation overhead per test.
192
- # run_via_event_loop and _invoke_via_fsm restart it lazily if it was reset.
193
- #
194
- # Preserve event_loop_stop_grace_seconds from the current configuration
195
- # so that test suites that set it to 0 (for fast teardown) keep that value
196
- # across configuration resets.
197
- prev_grace = @configuration&.event_loop_stop_grace_seconds
198
- @configuration = Configuration.new
199
- @configuration.event_loop_stop_grace_seconds = prev_grace if prev_grace
197
+ def reset_runtime!(timeout: configuration.event_loop_stop_grace_seconds)
198
+ previous_grace = @configuration&.event_loop_stop_grace_seconds
199
+ result = Runtime.reset_default!(timeout: timeout)
200
+
201
+ new_configuration = Configuration.new
202
+ if previous_grace
203
+ new_configuration.event_loop_stop_grace_seconds = previous_grace
204
+ end
205
+ @configuration = new_configuration
206
+ result
200
207
  end
201
208
  end
202
209
  end
@@ -29,11 +29,14 @@ PREAMBLE = <<~RUBY
29
29
  # Patch invoke methods to return canned responses instead of calling the LLM.
30
30
  module Phronomy
31
31
  module Agent
32
- class Base
32
+ # Prepend overrides AsyncEventApi (which is also prepended) so invoke
33
+ # returns a canned response without triggering the EventLoop.
34
+ module CiInvokeStub
33
35
  def invoke(input = nil, **)
34
36
  {output: "ci-stub-output", messages: []}
35
37
  end
36
38
  end
39
+ Base.prepend(CiInvokeStub)
37
40
 
38
41
  class Runner
39
42
  def invoke(input = nil, **)
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.13.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-23 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
@@ -66,18 +66,58 @@ dependencies:
66
66
  version: '0.6'
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: mcp
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: faraday
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '2'
88
+ - - "<"
89
+ - !ruby/object:Gem::Version
90
+ version: '3'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '2'
98
+ - - "<"
99
+ - !ruby/object:Gem::Version
100
+ version: '3'
101
+ - !ruby/object:Gem::Dependency
102
+ name: event_stream_parser
69
103
  requirement: !ruby/object:Gem::Requirement
70
104
  requirements:
71
105
  - - ">="
72
106
  - !ruby/object:Gem::Version
73
- version: '0.3'
107
+ version: '1'
108
+ - - "<"
109
+ - !ruby/object:Gem::Version
110
+ version: '2'
74
111
  type: :runtime
75
112
  prerelease: false
76
113
  version_requirements: !ruby/object:Gem::Requirement
77
114
  requirements:
78
115
  - - ">="
79
116
  - !ruby/object:Gem::Version
80
- version: '0.3'
117
+ version: '1'
118
+ - - "<"
119
+ - !ruby/object:Gem::Version
120
+ version: '2'
81
121
  description: Phronomy is a Ruby AI agent framework that provides composable building
82
122
  blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
83
123
  in Ruby. Powered by RubyLLM for LLM abstraction.
@@ -115,37 +155,44 @@ files:
115
155
  - docs/decisions/009-state-store-abstraction.md
116
156
  - docs/decisions/010-cooperative-first-concurrency.md
117
157
  - docs/decisions/011-build-context-as-single-llm-input-authority.md
158
+ - docs/decisions/011-delegate-transport-policy-to-adapters.md
159
+ - docs/mcp-client.md
160
+ - examples/workflows/agent_event_mapping.rb
161
+ - examples/workflows/generic_task_event_mapping.rb
162
+ - gemfiles/mcp_1_0.gemfile
118
163
  - lib/phronomy.rb
119
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
120
170
  - lib/phronomy/agent/base.rb
121
171
  - lib/phronomy/agent/before_completion_context.rb
122
172
  - lib/phronomy/agent/concerns/before_completion.rb
123
173
  - lib/phronomy/agent/concerns/error_translation.rb
124
174
  - lib/phronomy/agent/concerns/filterable.rb
125
- - lib/phronomy/agent/concerns/retryable.rb
126
175
  - lib/phronomy/agent/context/capability/base.rb
127
- - lib/phronomy/agent/context/capability/scope_policy.rb
128
176
  - lib/phronomy/agent/context/instruction/prompt_template.rb
129
177
  - lib/phronomy/agent/context/knowledge/base.rb
130
178
  - lib/phronomy/agent/context/knowledge/entity_knowledge.rb
131
179
  - lib/phronomy/agent/context/knowledge/static_knowledge.rb
132
- - lib/phronomy/agent/invocation_context.rb
133
- - lib/phronomy/agent/invocation_session.rb
180
+ - lib/phronomy/agent/llm_operation_result.rb
134
181
  - lib/phronomy/agent/phase_machine_builder.rb
135
182
  - lib/phronomy/agent/runner.rb
136
183
  - lib/phronomy/agent/shared_state.rb
137
- - lib/phronomy/agent/suspended_session_registry.rb
184
+ - lib/phronomy/agent/tool_approval_request.rb
138
185
  - lib/phronomy/agent/tool_call_intercepted.rb
139
186
  - lib/phronomy/agent/tool_executor.rb
187
+ - lib/phronomy/agent/tool_invocation.rb
188
+ - lib/phronomy/agent/tool_invocation_session_builder.rb
140
189
  - lib/phronomy/configuration.rb
141
190
  - lib/phronomy/diagnostics.rb
142
191
  - lib/phronomy/engine/concurrency/async_queue.rb
143
192
  - lib/phronomy/engine/concurrency/blocking_adapter_pool.rb
144
193
  - lib/phronomy/engine/concurrency/cancellation_scope.rb
145
194
  - lib/phronomy/engine/concurrency/cancellation_token.rb
146
- - lib/phronomy/engine/concurrency/concurrency_gate.rb
147
195
  - lib/phronomy/engine/concurrency/deadline.rb
148
- - lib/phronomy/engine/concurrency/gate_registry.rb
149
196
  - lib/phronomy/engine/concurrency/pool_registry.rb
150
197
  - lib/phronomy/engine/event_loop.rb
151
198
  - lib/phronomy/engine/fsm_session.rb
@@ -155,6 +202,7 @@ files:
155
202
  - lib/phronomy/engine/runtime/runtime_metrics.rb
156
203
  - lib/phronomy/engine/runtime/scheduler.rb
157
204
  - lib/phronomy/engine/runtime/scheduler_timer_adapter.rb
205
+ - lib/phronomy/engine/runtime/shutdown_result.rb
158
206
  - lib/phronomy/engine/runtime/task_registry.rb
159
207
  - lib/phronomy/engine/runtime/thread_scheduler.rb
160
208
  - lib/phronomy/engine/runtime/timer_queue.rb
@@ -184,6 +232,9 @@ files:
184
232
  - lib/phronomy/filter/base.rb
185
233
  - lib/phronomy/filter/prompt_injection_filter.rb
186
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
187
238
  - lib/phronomy/invocation_context.rb
188
239
  - lib/phronomy/knowledge_source.rb
189
240
  - lib/phronomy/llm_adapter.rb
@@ -206,6 +257,7 @@ files:
206
257
  - lib/phronomy/runnable.rb
207
258
  - lib/phronomy/state_store/base.rb
208
259
  - lib/phronomy/state_store/in_memory.rb
260
+ - lib/phronomy/stream_callback_error.rb
209
261
  - lib/phronomy/testing.rb
210
262
  - lib/phronomy/testing/fake_clock.rb
211
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