phronomy 0.11.0 → 0.11.1
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/README.md +17 -8
- data/benchmark/bench_agent_invoke.rb +1 -0
- data/lib/phronomy/agent/base.rb +192 -78
- data/lib/phronomy/agent/concerns/error_translation.rb +1 -1
- data/lib/phronomy/agent/concerns/retryable.rb +6 -2
- data/lib/phronomy/agent/invocation_context.rb +171 -0
- data/lib/phronomy/agent/invocation_session.rb +337 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +189 -0
- data/lib/phronomy/agent/suspended_session_registry.rb +54 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +26 -0
- data/lib/phronomy/configuration.rb +0 -6
- data/lib/phronomy/{concurrency → engine/concurrency}/async_queue.rb +68 -5
- data/lib/phronomy/{concurrency → engine/concurrency}/blocking_adapter_pool.rb +9 -3
- data/lib/phronomy/{event_loop.rb → engine/event_loop.rb} +71 -37
- data/lib/phronomy/engine/fsm_session.rb +248 -0
- data/lib/phronomy/{runtime → engine/runtime}/deterministic_scheduler.rb +28 -1
- data/lib/phronomy/{runtime → engine/runtime}/fake_scheduler.rb +1 -1
- data/lib/phronomy/{runtime.rb → engine/runtime.rb} +2 -4
- data/lib/phronomy/{task → engine/task}/backend.rb +2 -2
- data/lib/phronomy/engine/task/deferred_backend.rb +73 -0
- data/lib/phronomy/{task → engine/task}/fiber_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/immediate_backend.rb +1 -1
- data/lib/phronomy/{task → engine/task}/mapped_backend.rb +15 -3
- data/lib/phronomy/{task → engine/task}/thread_backend.rb +1 -1
- data/lib/phronomy/{task.rb → engine/task.rb} +19 -7
- data/lib/phronomy/eval/scorer/llm_judge.rb +1 -1
- data/lib/phronomy/generator_verifier.rb +20 -11
- data/lib/phronomy/multi_agent/orchestrator.rb +4 -4
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +3 -3
- data/lib/phronomy/tools/mcp.rb +1 -1
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +24 -13
- data/lib/phronomy/workflow.rb +6 -3
- data/lib/phronomy/workflow_context.rb +7 -5
- data/lib/phronomy/workflow_runner.rb +77 -27
- data/lib/phronomy.rb +17 -9
- metadata +35 -35
- data/lib/phronomy/agent/checkpoint.rb +0 -208
- data/lib/phronomy/agent/checkpoint_store.rb +0 -97
- data/lib/phronomy/agent/concerns/suspendable.rb +0 -190
- data/lib/phronomy/agent/suspend_signal.rb +0 -37
- data/lib/phronomy/context.rb +0 -12
- data/lib/phronomy/tool.rb +0 -9
- data/lib/phronomy/workflow/fsm_session.rb +0 -249
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_scope.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/cancellation_token.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/concurrency_gate.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/deadline.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/gate_registry.rb +0 -0
- /data/lib/phronomy/{concurrency → engine/concurrency}/pool_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/runtime_metrics.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/scheduler_timer_adapter.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/task_registry.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/thread_scheduler.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_queue.rb +0 -0
- /data/lib/phronomy/{runtime → engine/runtime}/timer_service.rb +0 -0
- /data/lib/phronomy/{task_group.rb → engine/task_group.rb} +0 -0
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.11.
|
|
4
|
+
version: 0.11.1
|
|
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-07-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -105,34 +105,54 @@ files:
|
|
|
105
105
|
- lib/phronomy/agent.rb
|
|
106
106
|
- lib/phronomy/agent/base.rb
|
|
107
107
|
- lib/phronomy/agent/before_completion_context.rb
|
|
108
|
-
- lib/phronomy/agent/checkpoint.rb
|
|
109
|
-
- lib/phronomy/agent/checkpoint_store.rb
|
|
110
108
|
- lib/phronomy/agent/concerns/before_completion.rb
|
|
111
109
|
- lib/phronomy/agent/concerns/error_translation.rb
|
|
112
110
|
- lib/phronomy/agent/concerns/filterable.rb
|
|
113
111
|
- lib/phronomy/agent/concerns/retryable.rb
|
|
114
|
-
- lib/phronomy/agent/concerns/suspendable.rb
|
|
115
112
|
- lib/phronomy/agent/context/capability/base.rb
|
|
116
113
|
- lib/phronomy/agent/context/capability/scope_policy.rb
|
|
117
114
|
- lib/phronomy/agent/context/instruction/prompt_template.rb
|
|
118
115
|
- lib/phronomy/agent/context/knowledge/base.rb
|
|
119
116
|
- lib/phronomy/agent/context/knowledge/entity_knowledge.rb
|
|
120
117
|
- lib/phronomy/agent/context/knowledge/static_knowledge.rb
|
|
118
|
+
- lib/phronomy/agent/invocation_context.rb
|
|
119
|
+
- lib/phronomy/agent/invocation_session.rb
|
|
120
|
+
- lib/phronomy/agent/phase_machine_builder.rb
|
|
121
121
|
- lib/phronomy/agent/runner.rb
|
|
122
122
|
- lib/phronomy/agent/shared_state.rb
|
|
123
|
-
- lib/phronomy/agent/
|
|
123
|
+
- lib/phronomy/agent/suspended_session_registry.rb
|
|
124
|
+
- lib/phronomy/agent/tool_call_intercepted.rb
|
|
124
125
|
- lib/phronomy/agent/tool_executor.rb
|
|
125
|
-
- lib/phronomy/concurrency/async_queue.rb
|
|
126
|
-
- lib/phronomy/concurrency/blocking_adapter_pool.rb
|
|
127
|
-
- lib/phronomy/concurrency/cancellation_scope.rb
|
|
128
|
-
- lib/phronomy/concurrency/cancellation_token.rb
|
|
129
|
-
- lib/phronomy/concurrency/concurrency_gate.rb
|
|
130
|
-
- lib/phronomy/concurrency/deadline.rb
|
|
131
|
-
- lib/phronomy/concurrency/gate_registry.rb
|
|
132
|
-
- lib/phronomy/concurrency/pool_registry.rb
|
|
133
126
|
- lib/phronomy/configuration.rb
|
|
134
|
-
- lib/phronomy/context.rb
|
|
135
127
|
- lib/phronomy/diagnostics.rb
|
|
128
|
+
- lib/phronomy/engine/concurrency/async_queue.rb
|
|
129
|
+
- lib/phronomy/engine/concurrency/blocking_adapter_pool.rb
|
|
130
|
+
- lib/phronomy/engine/concurrency/cancellation_scope.rb
|
|
131
|
+
- lib/phronomy/engine/concurrency/cancellation_token.rb
|
|
132
|
+
- lib/phronomy/engine/concurrency/concurrency_gate.rb
|
|
133
|
+
- lib/phronomy/engine/concurrency/deadline.rb
|
|
134
|
+
- lib/phronomy/engine/concurrency/gate_registry.rb
|
|
135
|
+
- lib/phronomy/engine/concurrency/pool_registry.rb
|
|
136
|
+
- lib/phronomy/engine/event_loop.rb
|
|
137
|
+
- lib/phronomy/engine/fsm_session.rb
|
|
138
|
+
- lib/phronomy/engine/runtime.rb
|
|
139
|
+
- lib/phronomy/engine/runtime/deterministic_scheduler.rb
|
|
140
|
+
- lib/phronomy/engine/runtime/fake_scheduler.rb
|
|
141
|
+
- lib/phronomy/engine/runtime/runtime_metrics.rb
|
|
142
|
+
- lib/phronomy/engine/runtime/scheduler.rb
|
|
143
|
+
- lib/phronomy/engine/runtime/scheduler_timer_adapter.rb
|
|
144
|
+
- lib/phronomy/engine/runtime/task_registry.rb
|
|
145
|
+
- lib/phronomy/engine/runtime/thread_scheduler.rb
|
|
146
|
+
- lib/phronomy/engine/runtime/timer_queue.rb
|
|
147
|
+
- lib/phronomy/engine/runtime/timer_service.rb
|
|
148
|
+
- lib/phronomy/engine/task.rb
|
|
149
|
+
- lib/phronomy/engine/task/backend.rb
|
|
150
|
+
- lib/phronomy/engine/task/deferred_backend.rb
|
|
151
|
+
- lib/phronomy/engine/task/fiber_backend.rb
|
|
152
|
+
- lib/phronomy/engine/task/immediate_backend.rb
|
|
153
|
+
- lib/phronomy/engine/task/mapped_backend.rb
|
|
154
|
+
- lib/phronomy/engine/task/thread_backend.rb
|
|
155
|
+
- lib/phronomy/engine/task_group.rb
|
|
136
156
|
- lib/phronomy/eval.rb
|
|
137
157
|
- lib/phronomy/eval/comparison.rb
|
|
138
158
|
- lib/phronomy/eval/dataset.rb
|
|
@@ -146,7 +166,6 @@ files:
|
|
|
146
166
|
- lib/phronomy/eval/scorer/includes_scorer.rb
|
|
147
167
|
- lib/phronomy/eval/scorer/llm_judge.rb
|
|
148
168
|
- lib/phronomy/event.rb
|
|
149
|
-
- lib/phronomy/event_loop.rb
|
|
150
169
|
- lib/phronomy/filter.rb
|
|
151
170
|
- lib/phronomy/filter/base.rb
|
|
152
171
|
- lib/phronomy/filter/prompt_injection_filter.rb
|
|
@@ -171,31 +190,13 @@ files:
|
|
|
171
190
|
- lib/phronomy/output_parser/structured_parser.rb
|
|
172
191
|
- lib/phronomy/ruby_llm_patches.rb
|
|
173
192
|
- lib/phronomy/runnable.rb
|
|
174
|
-
- lib/phronomy/runtime.rb
|
|
175
|
-
- lib/phronomy/runtime/deterministic_scheduler.rb
|
|
176
|
-
- lib/phronomy/runtime/fake_scheduler.rb
|
|
177
|
-
- lib/phronomy/runtime/runtime_metrics.rb
|
|
178
|
-
- lib/phronomy/runtime/scheduler.rb
|
|
179
|
-
- lib/phronomy/runtime/scheduler_timer_adapter.rb
|
|
180
|
-
- lib/phronomy/runtime/task_registry.rb
|
|
181
|
-
- lib/phronomy/runtime/thread_scheduler.rb
|
|
182
|
-
- lib/phronomy/runtime/timer_queue.rb
|
|
183
|
-
- lib/phronomy/runtime/timer_service.rb
|
|
184
193
|
- lib/phronomy/state_store/base.rb
|
|
185
194
|
- lib/phronomy/state_store/in_memory.rb
|
|
186
|
-
- lib/phronomy/task.rb
|
|
187
|
-
- lib/phronomy/task/backend.rb
|
|
188
|
-
- lib/phronomy/task/fiber_backend.rb
|
|
189
|
-
- lib/phronomy/task/immediate_backend.rb
|
|
190
|
-
- lib/phronomy/task/mapped_backend.rb
|
|
191
|
-
- lib/phronomy/task/thread_backend.rb
|
|
192
|
-
- lib/phronomy/task_group.rb
|
|
193
195
|
- lib/phronomy/testing.rb
|
|
194
196
|
- lib/phronomy/testing/fake_clock.rb
|
|
195
197
|
- lib/phronomy/testing/fake_scheduler.rb
|
|
196
198
|
- lib/phronomy/testing/scheduler_helpers.rb
|
|
197
199
|
- lib/phronomy/token_usage.rb
|
|
198
|
-
- lib/phronomy/tool.rb
|
|
199
200
|
- lib/phronomy/tools/agent.rb
|
|
200
201
|
- lib/phronomy/tools/mcp.rb
|
|
201
202
|
- lib/phronomy/tools/vector_search.rb
|
|
@@ -221,7 +222,6 @@ files:
|
|
|
221
222
|
- lib/phronomy/vector_store/splitter/recursive_splitter.rb
|
|
222
223
|
- lib/phronomy/version.rb
|
|
223
224
|
- lib/phronomy/workflow.rb
|
|
224
|
-
- lib/phronomy/workflow/fsm_session.rb
|
|
225
225
|
- lib/phronomy/workflow/phase_machine_builder.rb
|
|
226
226
|
- lib/phronomy/workflow_context.rb
|
|
227
227
|
- lib/phronomy/workflow_runner.rb
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module Agent
|
|
7
|
-
# Encapsulates the suspended state of an agent invocation.
|
|
8
|
-
#
|
|
9
|
-
# A Checkpoint is returned as the +:checkpoint+ key of the result hash when
|
|
10
|
-
# an approval-required tool is encountered and no synchronous
|
|
11
|
-
# on_approval_required handler has been registered.
|
|
12
|
-
#
|
|
13
|
-
# Pass the checkpoint to Agent::Base#resume to continue execution after
|
|
14
|
-
# obtaining an approval decision from the user or an external system.
|
|
15
|
-
#
|
|
16
|
-
# @example Suspend and resume
|
|
17
|
-
# result = agent.invoke("Do task X")
|
|
18
|
-
# if result[:suspended]
|
|
19
|
-
# approved = prompt_user(result[:checkpoint].pending_tool_name)
|
|
20
|
-
# result = agent.resume(result[:checkpoint], approved: approved)
|
|
21
|
-
# end
|
|
22
|
-
# puts result[:output]
|
|
23
|
-
class Checkpoint
|
|
24
|
-
# @return [String] a globally unique identifier for this checkpoint;
|
|
25
|
-
# used as an idempotency key when guarding against duplicate resumes
|
|
26
|
-
attr_reader :checkpoint_id
|
|
27
|
-
|
|
28
|
-
# @return [String, nil] the fully-qualified name of the agent class that
|
|
29
|
-
# created this checkpoint (e.g. +"MyApp::ReviewAgent"+); used by the
|
|
30
|
-
# class-level +resume+ method to validate the correct agent is used
|
|
31
|
-
attr_reader :agent_class
|
|
32
|
-
|
|
33
|
-
# @return [Time] the UTC timestamp when this checkpoint was created
|
|
34
|
-
attr_reader :requested_at
|
|
35
|
-
|
|
36
|
-
# @return [String, nil] the thread_id from the invocation config
|
|
37
|
-
attr_reader :thread_id
|
|
38
|
-
|
|
39
|
-
# @return [String, Hash] the original input passed to #invoke; stored so
|
|
40
|
-
# that #resume can re-apply dynamic system instructions (e.g. Proc or
|
|
41
|
-
# PromptTemplate-based instructions that depend on the input value).
|
|
42
|
-
attr_reader :original_input
|
|
43
|
-
|
|
44
|
-
# @return [Array<RubyLLM::Message>] conversation messages up to and including
|
|
45
|
-
# the assistant message that requested the pending tool call
|
|
46
|
-
attr_reader :messages
|
|
47
|
-
|
|
48
|
-
# @return [String] the name of the tool awaiting approval
|
|
49
|
-
attr_reader :pending_tool_name
|
|
50
|
-
|
|
51
|
-
# @return [Hash] the arguments the LLM passed to the pending tool
|
|
52
|
-
attr_reader :pending_tool_args
|
|
53
|
-
|
|
54
|
-
# @return [String] the tool_call_id from the LLM response (required to
|
|
55
|
-
# inject the tool result message on resume)
|
|
56
|
-
attr_reader :pending_tool_call_id
|
|
57
|
-
|
|
58
|
-
# @param checkpoint_id [String] unique identifier; defaults to a new UUID
|
|
59
|
-
# @param agent_class [String, nil] fully-qualified agent class name
|
|
60
|
-
# @param requested_at [Time] when the checkpoint was created; defaults to +Time.now.utc+
|
|
61
|
-
# @param thread_id [String, nil]
|
|
62
|
-
# @param original_input [String, Hash] the input passed to the original #invoke call
|
|
63
|
-
# @param messages [Array<RubyLLM::Message>]
|
|
64
|
-
# @param pending_tool_name [String]
|
|
65
|
-
# @param pending_tool_args [Hash]
|
|
66
|
-
# @param pending_tool_call_id [String]
|
|
67
|
-
# @api public
|
|
68
|
-
def initialize(thread_id:, original_input:, messages:, pending_tool_name:, pending_tool_args:, pending_tool_call_id:,
|
|
69
|
-
checkpoint_id: SecureRandom.uuid, agent_class: nil, requested_at: Time.now.utc)
|
|
70
|
-
@checkpoint_id = checkpoint_id
|
|
71
|
-
@agent_class = agent_class
|
|
72
|
-
@requested_at = requested_at
|
|
73
|
-
@thread_id = thread_id
|
|
74
|
-
@original_input = original_input
|
|
75
|
-
@messages = messages.dup.freeze
|
|
76
|
-
@pending_tool_name = pending_tool_name
|
|
77
|
-
@pending_tool_args = pending_tool_args
|
|
78
|
-
@pending_tool_call_id = pending_tool_call_id
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Converts this checkpoint to a plain Hash suitable for JSON / Marshal serialization.
|
|
82
|
-
#
|
|
83
|
-
# All values are plain Ruby objects (String, Symbol, Hash, Array, Numeric,
|
|
84
|
-
# nil). +RubyLLM::Message+ objects in +:messages+ are deep-converted so that
|
|
85
|
-
# any embedded +RubyLLM::ToolCall+ objects are also serialized as plain hashes.
|
|
86
|
-
#
|
|
87
|
-
# @example Round-trip via JSON
|
|
88
|
-
# json = JSON.generate(checkpoint.to_h)
|
|
89
|
-
# checkpoint2 = Phronomy::Agent::Checkpoint.from_h(JSON.parse(json))
|
|
90
|
-
#
|
|
91
|
-
# @return [Hash]
|
|
92
|
-
# @api public
|
|
93
|
-
def to_h
|
|
94
|
-
{
|
|
95
|
-
checkpoint_id: @checkpoint_id,
|
|
96
|
-
agent_class: @agent_class,
|
|
97
|
-
requested_at: @requested_at&.iso8601,
|
|
98
|
-
thread_id: @thread_id,
|
|
99
|
-
original_input: @original_input,
|
|
100
|
-
messages: @messages.map { |m| serialize_message(m) },
|
|
101
|
-
pending_tool_name: @pending_tool_name,
|
|
102
|
-
pending_tool_args: @pending_tool_args,
|
|
103
|
-
pending_tool_call_id: @pending_tool_call_id
|
|
104
|
-
}
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Reconstructs a +Checkpoint+ from a plain Hash (e.g. produced by {#to_h}
|
|
108
|
-
# and deserialized from JSON or Marshal).
|
|
109
|
-
#
|
|
110
|
-
# Hash keys may be either Symbols or Strings; both are accepted.
|
|
111
|
-
# +RubyLLM::ToolCall+ objects inside message +:tool_calls+ arrays are
|
|
112
|
-
# reconstructed from their hash representations.
|
|
113
|
-
#
|
|
114
|
-
# @param h [Hash] a hash previously produced by {#to_h}
|
|
115
|
-
# @return [Checkpoint]
|
|
116
|
-
# @api public
|
|
117
|
-
def self.from_h(h)
|
|
118
|
-
h = h.transform_keys { |k|
|
|
119
|
-
begin
|
|
120
|
-
k.to_sym
|
|
121
|
-
rescue
|
|
122
|
-
k
|
|
123
|
-
end
|
|
124
|
-
}
|
|
125
|
-
messages = Array(h[:messages]).map { |m| deserialize_message(m) }
|
|
126
|
-
requested_at_raw = h[:requested_at]
|
|
127
|
-
requested_at = requested_at_raw ? Time.parse(requested_at_raw.to_s).utc : nil
|
|
128
|
-
new(
|
|
129
|
-
checkpoint_id: h[:checkpoint_id]&.to_s || SecureRandom.uuid,
|
|
130
|
-
agent_class: h[:agent_class]&.to_s,
|
|
131
|
-
requested_at: requested_at || Time.now.utc,
|
|
132
|
-
thread_id: h[:thread_id],
|
|
133
|
-
original_input: h[:original_input],
|
|
134
|
-
messages: messages,
|
|
135
|
-
pending_tool_name: h[:pending_tool_name]&.to_s,
|
|
136
|
-
pending_tool_args: h[:pending_tool_args] ? h[:pending_tool_args].transform_keys { |k|
|
|
137
|
-
begin
|
|
138
|
-
k.to_sym
|
|
139
|
-
rescue
|
|
140
|
-
k
|
|
141
|
-
end
|
|
142
|
-
} : {},
|
|
143
|
-
pending_tool_call_id: h[:pending_tool_call_id]&.to_s
|
|
144
|
-
)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
# Converts a +RubyLLM::Message+ to a plain Hash, ensuring that any
|
|
150
|
-
# embedded +RubyLLM::ToolCall+ objects in +:tool_calls+ are also converted.
|
|
151
|
-
#
|
|
152
|
-
# @param msg [RubyLLM::Message]
|
|
153
|
-
# @return [Hash]
|
|
154
|
-
# @api private
|
|
155
|
-
def serialize_message(msg)
|
|
156
|
-
h = msg.to_h
|
|
157
|
-
return h unless h[:tool_calls]
|
|
158
|
-
|
|
159
|
-
h.merge(tool_calls: h[:tool_calls].map { |tc|
|
|
160
|
-
tc.respond_to?(:to_h) ? tc.to_h : tc
|
|
161
|
-
})
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Reconstructs a +RubyLLM::Message+ from a plain Hash.
|
|
165
|
-
# +RubyLLM::ToolCall+ entries in +:tool_calls+ are re-instantiated.
|
|
166
|
-
#
|
|
167
|
-
# @param h [Hash]
|
|
168
|
-
# @return [RubyLLM::Message]
|
|
169
|
-
# @api private
|
|
170
|
-
def self.deserialize_message(h)
|
|
171
|
-
h = h.transform_keys { |k|
|
|
172
|
-
begin
|
|
173
|
-
k.to_sym
|
|
174
|
-
rescue
|
|
175
|
-
k
|
|
176
|
-
end
|
|
177
|
-
}
|
|
178
|
-
if h[:tool_calls]
|
|
179
|
-
h = h.merge(tool_calls: Array(h[:tool_calls]).map { |tc|
|
|
180
|
-
next tc if tc.is_a?(RubyLLM::ToolCall)
|
|
181
|
-
|
|
182
|
-
tc = tc.transform_keys { |k|
|
|
183
|
-
begin
|
|
184
|
-
k.to_sym
|
|
185
|
-
rescue
|
|
186
|
-
k
|
|
187
|
-
end
|
|
188
|
-
}
|
|
189
|
-
RubyLLM::ToolCall.new(
|
|
190
|
-
id: tc[:id].to_s,
|
|
191
|
-
name: tc[:name].to_s,
|
|
192
|
-
arguments: (tc[:arguments] || {}).transform_keys { |k|
|
|
193
|
-
begin
|
|
194
|
-
k.to_sym
|
|
195
|
-
rescue
|
|
196
|
-
k
|
|
197
|
-
end
|
|
198
|
-
},
|
|
199
|
-
thought_signature: tc[:thought_signature]
|
|
200
|
-
)
|
|
201
|
-
})
|
|
202
|
-
end
|
|
203
|
-
RubyLLM::Message.new(h)
|
|
204
|
-
end
|
|
205
|
-
private_class_method :deserialize_message
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# Default in-memory idempotency store for {Checkpoint} resume operations.
|
|
6
|
-
#
|
|
7
|
-
# Tracks consumed checkpoint IDs so that calling {Agent::Base#resume} twice
|
|
8
|
-
# with the same checkpoint raises {Phronomy::CheckpointAlreadyResumedError}
|
|
9
|
-
# instead of silently executing the approved tool a second time.
|
|
10
|
-
#
|
|
11
|
-
# This implementation is *not thread-safe*. It assumes a single agent instance
|
|
12
|
-
# is accessed from only one thread at a time, which is the expected usage pattern.
|
|
13
|
-
# Agent instances themselves are not thread-safe (state like +@messages+, +@config+
|
|
14
|
-
# is not protected), so concurrent calls to the same agent instance are unsupported.
|
|
15
|
-
#
|
|
16
|
-
# Each agent instance gets its own store by default, so no sharing occurs unless
|
|
17
|
-
# the caller explicitly assigns the same store object to multiple agents.
|
|
18
|
-
#
|
|
19
|
-
# For distributed environments (multiple processes or background jobs), swap this
|
|
20
|
-
# for a custom implementation backed by Redis, ActiveRecord, or another shared store.
|
|
21
|
-
# *Your custom store implementation is responsible for ensuring thread-safety* if
|
|
22
|
-
# your application shares the same store instance across multiple threads.
|
|
23
|
-
#
|
|
24
|
-
# @example Plugging in a custom store
|
|
25
|
-
# agent = MyAgent.new
|
|
26
|
-
# agent.checkpoint_store = MyRedis::CheckpointStore.new
|
|
27
|
-
#
|
|
28
|
-
# @example Duck-type contract required by any replacement
|
|
29
|
-
# # consumed?(checkpoint_id) => Boolean
|
|
30
|
-
# # consume!(checkpoint_id) => void; raises CheckpointAlreadyResumedError if duplicate
|
|
31
|
-
# # cleanup!(checkpoint_id) => void (optional); removes tracking for the checkpoint
|
|
32
|
-
# # clear! => void (optional); removes all tracked checkpoints
|
|
33
|
-
#
|
|
34
|
-
# @api public
|
|
35
|
-
class CheckpointStore
|
|
36
|
-
def initialize
|
|
37
|
-
@consumed = Set.new
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Returns +true+ if the given checkpoint ID has already been consumed.
|
|
41
|
-
#
|
|
42
|
-
# @param checkpoint_id [String]
|
|
43
|
-
# @return [Boolean]
|
|
44
|
-
# @api public
|
|
45
|
-
def consumed?(checkpoint_id)
|
|
46
|
-
@consumed.include?(checkpoint_id)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Marks +checkpoint_id+ as consumed, or raises if it was already consumed.
|
|
50
|
-
#
|
|
51
|
-
# @param checkpoint_id [String]
|
|
52
|
-
# @raise [Phronomy::CheckpointAlreadyResumedError]
|
|
53
|
-
# @return [void]
|
|
54
|
-
# @api public
|
|
55
|
-
def consume!(checkpoint_id)
|
|
56
|
-
if @consumed.include?(checkpoint_id)
|
|
57
|
-
raise Phronomy::CheckpointAlreadyResumedError,
|
|
58
|
-
"checkpoint #{checkpoint_id} has already been resumed"
|
|
59
|
-
end
|
|
60
|
-
@consumed.add(checkpoint_id)
|
|
61
|
-
nil
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Removes tracking for a specific checkpoint ID.
|
|
65
|
-
#
|
|
66
|
-
# Use this to explicitly discard a checkpoint when the application
|
|
67
|
-
# determines it is no longer needed (e.g., user abandons an approval
|
|
68
|
-
# workflow).
|
|
69
|
-
#
|
|
70
|
-
# This method is optional in the duck-type contract. Custom store
|
|
71
|
-
# implementations may choose not to implement it.
|
|
72
|
-
#
|
|
73
|
-
# @param checkpoint_id [String]
|
|
74
|
-
# @return [void]
|
|
75
|
-
# @api public
|
|
76
|
-
def cleanup!(checkpoint_id)
|
|
77
|
-
@consumed.delete(checkpoint_id)
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Removes all tracked checkpoint IDs.
|
|
82
|
-
#
|
|
83
|
-
# Use this for test cleanup, periodic maintenance, or application
|
|
84
|
-
# shutdown.
|
|
85
|
-
#
|
|
86
|
-
# This method is optional in the duck-type contract. Custom store
|
|
87
|
-
# implementations may choose not to implement it.
|
|
88
|
-
#
|
|
89
|
-
# @return [void]
|
|
90
|
-
# @api public
|
|
91
|
-
def clear!
|
|
92
|
-
@consumed.clear
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module Agent
|
|
7
|
-
module Concerns
|
|
8
|
-
# Adds suspend/resume and tool-approval support to an agent.
|
|
9
|
-
#
|
|
10
|
-
# Included in {Phronomy::Agent::Base}. When a tool decorated with
|
|
11
|
-
# +requires_approval true+ is called and no synchronous approval handler
|
|
12
|
-
# has been registered, the invocation is suspended and a
|
|
13
|
-
# @api private
|
|
14
|
-
# {Phronomy::Agent::Checkpoint} is returned so the caller can resume later.
|
|
15
|
-
module Suspendable
|
|
16
|
-
# Registers a callback that is invoked before executing any tool that has
|
|
17
|
-
# +requires_approval true+ set. The block receives the tool name (String)
|
|
18
|
-
# and the arguments Hash, and must return a truthy value to allow execution.
|
|
19
|
-
# Returning a falsy value causes the tool to return a denial message instead
|
|
20
|
-
# of executing.
|
|
21
|
-
#
|
|
22
|
-
# When no handler is registered and a tool with +requires_approval+ is
|
|
23
|
-
# called, #invoke returns a suspended result hash containing a
|
|
24
|
-
# {Phronomy::Agent::Checkpoint}. Call #resume to continue execution after
|
|
25
|
-
# obtaining an approval decision from the user or an external system.
|
|
26
|
-
#
|
|
27
|
-
# @example Synchronous handler
|
|
28
|
-
# agent = MyAgent.new
|
|
29
|
-
# agent.on_approval_required { |tool_name, args| prompt_user(tool_name, args) }
|
|
30
|
-
# @return [self]
|
|
31
|
-
# @api private
|
|
32
|
-
def on_approval_required(&block)
|
|
33
|
-
@approval_handler = block
|
|
34
|
-
self
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Registers a scope policy callable for this agent instance.
|
|
38
|
-
#
|
|
39
|
-
# The callable receives +(tool_class, scope, agent)+ and must return
|
|
40
|
-
# +:allow+, +:reject+, or +:approve+.
|
|
41
|
-
#
|
|
42
|
-
# @example Reject all write-scoped tools
|
|
43
|
-
# agent.scope_policy = ->(_tc, scope, _agent) { scope == :write ? :reject : :allow }
|
|
44
|
-
#
|
|
45
|
-
# @param policy [#call]
|
|
46
|
-
# @return [void]
|
|
47
|
-
# @api public
|
|
48
|
-
def scope_policy=(policy)
|
|
49
|
-
@scope_policy = policy
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Sets the idempotency store used to guard against duplicate resumes.
|
|
53
|
-
#
|
|
54
|
-
# The store must respond to:
|
|
55
|
-
# - +consumed?(checkpoint_id)+ ⇒ Boolean
|
|
56
|
-
# - +consume!(checkpoint_id)+ ⇒ void; raises {Phronomy::CheckpointAlreadyResumedError} on duplicate
|
|
57
|
-
#
|
|
58
|
-
# Defaults to a per-instance {Phronomy::Agent::CheckpointStore} (in-memory, not thread-safe).
|
|
59
|
-
# Assign a shared persistent store when resuming across processes (e.g. Redis-backed).
|
|
60
|
-
# Custom stores are responsible for ensuring thread-safety if shared across threads.
|
|
61
|
-
#
|
|
62
|
-
# @param store [#consumed?, #consume!]
|
|
63
|
-
# @return [void]
|
|
64
|
-
# @api public
|
|
65
|
-
def checkpoint_store=(store)
|
|
66
|
-
@checkpoint_store = store
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Resumes a previously suspended invocation from a {Phronomy::Agent::Checkpoint}.
|
|
70
|
-
#
|
|
71
|
-
# This method reconstructs the conversation state captured at suspension
|
|
72
|
-
# time, injects the tool result (executed or denied), and continues the
|
|
73
|
-
# LLM loop until it produces a final answer.
|
|
74
|
-
#
|
|
75
|
-
# @param checkpoint [Phronomy::Agent::Checkpoint] the checkpoint returned by
|
|
76
|
-
# the suspended #invoke call
|
|
77
|
-
# @param approved [Boolean] +true+ to execute the pending tool; +false+
|
|
78
|
-
# to inject a denial message and let the LLM handle it gracefully
|
|
79
|
-
# @param config [Hash] same runtime options as #invoke
|
|
80
|
-
# @return [Hash] +{ output: String, suspended: false, messages: Array, usage: Phronomy::TokenUsage }+
|
|
81
|
-
# or +{ output: nil, suspended: true, checkpoint: Phronomy::Agent::Checkpoint, messages: Array }+
|
|
82
|
-
# when a second approval-required tool is encountered during continuation
|
|
83
|
-
# @raise [Phronomy::FilterBlockError] when an output filter rejects the value
|
|
84
|
-
# @raise [Phronomy::CheckpointAlreadyResumedError] when the checkpoint has already been consumed
|
|
85
|
-
# @api private
|
|
86
|
-
def resume(checkpoint, approved:, config: {})
|
|
87
|
-
# Guard against duplicate resumes using the idempotency store.
|
|
88
|
-
_checkpoint_store.consume!(checkpoint.checkpoint_id)
|
|
89
|
-
# Build a fresh chat with all tools registered.
|
|
90
|
-
chat = build_chat
|
|
91
|
-
|
|
92
|
-
# Re-apply system instructions and register tools so the LLM has the
|
|
93
|
-
# same persona/context as the original invocation. build_context
|
|
94
|
-
# includes all tool classes (static + handoff) via add_capability.
|
|
95
|
-
context = build_context(checkpoint.original_input, messages: [])
|
|
96
|
-
apply_instructions(chat, context[:system]) if context[:system]
|
|
97
|
-
(context[:tool_classes] || []).each { |tc| chat.with_tool(prepare_tool_class(tc)) }
|
|
98
|
-
|
|
99
|
-
# Restore the full conversation (history + user + assistant with tool call).
|
|
100
|
-
checkpoint.messages.each { |msg| chat.messages << msg }
|
|
101
|
-
|
|
102
|
-
# Determine the tool result: execute it or inject a denial string.
|
|
103
|
-
tool_result =
|
|
104
|
-
if approved
|
|
105
|
-
tool_instance = chat.tools[checkpoint.pending_tool_name.to_sym]
|
|
106
|
-
tool_instance ? tool_instance.call(checkpoint.pending_tool_args) : "Tool not found."
|
|
107
|
-
else
|
|
108
|
-
"Tool execution denied."
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Inject the tool result so the LLM can continue.
|
|
112
|
-
chat.add_message(
|
|
113
|
-
role: :tool,
|
|
114
|
-
content: tool_result.to_s,
|
|
115
|
-
tool_call_id: checkpoint.pending_tool_call_id
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# Re-register the suspension hook so that any further requires_approval
|
|
119
|
-
# tools encountered during continuation are intercepted rather than
|
|
120
|
-
# executed without approval (cascading / chained approval scenario).
|
|
121
|
-
_register_suspension_hook!(chat)
|
|
122
|
-
|
|
123
|
-
# Continue the LLM loop. Rescue SuspendSignal so that a second
|
|
124
|
-
# approval-required tool produces a new checkpoint instead of running
|
|
125
|
-
# without consent.
|
|
126
|
-
begin
|
|
127
|
-
response = chat.complete
|
|
128
|
-
rescue SuspendSignal => signal
|
|
129
|
-
new_checkpoint = Checkpoint.new(
|
|
130
|
-
checkpoint_id: SecureRandom.uuid,
|
|
131
|
-
agent_class: self.class.name,
|
|
132
|
-
requested_at: Time.now.utc,
|
|
133
|
-
thread_id: checkpoint.thread_id,
|
|
134
|
-
original_input: checkpoint.original_input,
|
|
135
|
-
messages: chat.messages.dup,
|
|
136
|
-
pending_tool_name: signal.tool_name,
|
|
137
|
-
pending_tool_args: signal.args,
|
|
138
|
-
pending_tool_call_id: signal.tool_call_id
|
|
139
|
-
)
|
|
140
|
-
return {output: nil, suspended: true, checkpoint: new_checkpoint, messages: chat.messages}
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
output = response.content
|
|
144
|
-
usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
145
|
-
|
|
146
|
-
output = run_output_filters!(output)
|
|
147
|
-
|
|
148
|
-
{output: output, suspended: false, messages: chat.messages, usage: usage}
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
# Registers an on_tool_call hook on the chat object that raises SuspendSignal
|
|
154
|
-
# when an approval-required tool is about to be executed and no synchronous
|
|
155
|
-
# on_approval_required handler has been registered.
|
|
156
|
-
#
|
|
157
|
-
# Does nothing when:
|
|
158
|
-
# - a synchronous handler is already registered (@approval_handler is set), or
|
|
159
|
-
# - none of the agent's tools have requires_approval set.
|
|
160
|
-
#
|
|
161
|
-
# @param chat [RubyLLM::Chat]
|
|
162
|
-
# @api private
|
|
163
|
-
def _register_suspension_hook!(chat)
|
|
164
|
-
return if @approval_handler
|
|
165
|
-
return if self.class.tools.none? { |tc| tc.requires_approval }
|
|
166
|
-
|
|
167
|
-
chat.on_tool_call do |tool_call|
|
|
168
|
-
tool_instance = chat.tools[tool_call.name.to_sym]
|
|
169
|
-
if tool_instance&.requires_approval
|
|
170
|
-
raise SuspendSignal.new(
|
|
171
|
-
tool_name: tool_call.name,
|
|
172
|
-
args: tool_call.arguments,
|
|
173
|
-
tool_call_id: tool_call.id
|
|
174
|
-
)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Returns the checkpoint idempotency store for this instance, lazily
|
|
180
|
-
# initialising a default in-memory {Phronomy::Agent::CheckpointStore}.
|
|
181
|
-
#
|
|
182
|
-
# @return [#consumed?, #consume!]
|
|
183
|
-
# @api private
|
|
184
|
-
def _checkpoint_store
|
|
185
|
-
@checkpoint_store ||= CheckpointStore.new
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# Raised internally inside the on_tool_call hook when an approval-required
|
|
6
|
-
# tool is encountered and no synchronous on_approval_required handler has
|
|
7
|
-
# been registered. Caught by Agent::Base#invoke_once to produce a
|
|
8
|
-
# suspended result hash containing a Checkpoint.
|
|
9
|
-
#
|
|
10
|
-
# This class is intentionally NOT part of the public API. Callers should
|
|
11
|
-
# @api private
|
|
12
|
-
# inspect the +:suspended+ key in the result hash returned by #invoke.
|
|
13
|
-
#
|
|
14
|
-
# @api private
|
|
15
|
-
class SuspendSignal < StandardError
|
|
16
|
-
# @return [String] the name of the tool that triggered the suspension
|
|
17
|
-
attr_reader :tool_name
|
|
18
|
-
|
|
19
|
-
# @return [Hash] the arguments the LLM passed to the tool
|
|
20
|
-
attr_reader :args
|
|
21
|
-
|
|
22
|
-
# @return [String] the tool_call_id from the LLM response
|
|
23
|
-
attr_reader :tool_call_id
|
|
24
|
-
|
|
25
|
-
# @param tool_name [String]
|
|
26
|
-
# @param args [Hash]
|
|
27
|
-
# @param tool_call_id [String]
|
|
28
|
-
# @api private
|
|
29
|
-
def initialize(tool_name:, args:, tool_call_id:)
|
|
30
|
-
super("Agent suspended waiting for approval of tool: #{tool_name}")
|
|
31
|
-
@tool_name = tool_name
|
|
32
|
-
@args = args
|
|
33
|
-
@tool_call_id = tool_call_id
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
data/lib/phronomy/context.rb
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
# Context management utilities: token estimation, budget calculation, and
|
|
5
|
-
# context assembly.
|
|
6
|
-
#
|
|
7
|
-
# Sub-modules are auto-loaded by Zeitwerk:
|
|
8
|
-
# Phronomy::LlmContextWindow::TokenEstimator
|
|
9
|
-
# Phronomy::LlmContextWindow::TokenBudget
|
|
10
|
-
module Context
|
|
11
|
-
end
|
|
12
|
-
end
|
data/lib/phronomy/tool.rb
DELETED