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
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
122
|
-
#
|
|
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
|
-
#
|
|
179
|
-
#
|
|
187
|
+
# Shuts down and clears the process-wide default Runtime, then resets
|
|
188
|
+
# global configuration. Intended for test suites only.
|
|
180
189
|
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
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
|
-
# @
|
|
186
|
-
#
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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: '
|
|
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: '
|
|
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/
|
|
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/
|
|
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
|