phronomy 0.10.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/CHANGELOG.md +41 -1
- data/README.md +35 -24
- data/benchmark/bench_agent_invoke.rb +1 -0
- data/lib/phronomy/agent/base.rb +223 -95
- data/lib/phronomy/agent/concerns/error_translation.rb +1 -1
- data/lib/phronomy/agent/concerns/filterable.rb +184 -0
- data/lib/phronomy/agent/concerns/retryable.rb +9 -5
- data/lib/phronomy/agent/context/capability/base.rb +2 -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/filter/base.rb +56 -0
- data/lib/phronomy/{guardrail/prompt_injection_guardrail.rb → filter/prompt_injection_filter.rb} +20 -13
- data/lib/phronomy/filter.rb +5 -0
- data/lib/phronomy/generator_verifier.rb +21 -13
- data/lib/phronomy/multi_agent/orchestrator.rb +4 -4
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +3 -3
- data/lib/phronomy/multi_agent/team_coordinator.rb +1 -2
- 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 +24 -13
- data/scripts/api_snapshot.rb +2 -2
- metadata +42 -44
- data/lib/phronomy/agent/checkpoint.rb +0 -208
- data/lib/phronomy/agent/checkpoint_store.rb +0 -97
- data/lib/phronomy/agent/concerns/guardrailable.rb +0 -45
- 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/guardrail/base.rb +0 -45
- data/lib/phronomy/guardrail/input_guardrail.rb +0 -19
- data/lib/phronomy/guardrail/output_guardrail.rb +0 -19
- data/lib/phronomy/guardrail.rb +0 -7
- 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
|
@@ -79,29 +79,50 @@ module Phronomy
|
|
|
79
79
|
caller_meta[:session_id] = config[:session_id] if config[:session_id]
|
|
80
80
|
|
|
81
81
|
trace("workflow.invoke", input: input.inspect, **caller_meta) do |_span|
|
|
82
|
-
thread_id = config
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
store = config.fetch(:state_store, @state_store) || Phronomy.configuration.state_store
|
|
86
|
-
snapshot = (store && config[:thread_id]) ? store.load(thread_id) : nil
|
|
87
|
-
initial_fields = if snapshot && snapshot[:fields]
|
|
88
|
-
snapshot[:fields].transform_keys(&:to_sym).merge(input.transform_keys(&:to_sym))
|
|
89
|
-
else
|
|
90
|
-
input
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
state = @state_class.new(**initial_fields)
|
|
94
|
-
state.set_graph_metadata(thread_id: thread_id)
|
|
95
|
-
result = if Phronomy.configuration.event_loop
|
|
96
|
-
run_via_event_loop(state, recursion_limit: recursion_limit)
|
|
97
|
-
else
|
|
98
|
-
run_workflow(state, recursion_limit: recursion_limit)
|
|
99
|
-
end
|
|
82
|
+
state, thread_id, recursion_limit, store = _build_initial_context(input, config)
|
|
83
|
+
result = run_via_event_loop(state, recursion_limit: recursion_limit)
|
|
100
84
|
store&.save(thread_id, {fields: result.to_h, phase: result.phase.to_s}) if config[:thread_id]
|
|
101
85
|
[result, nil]
|
|
102
86
|
end
|
|
103
87
|
end
|
|
104
88
|
|
|
89
|
+
# Registers the workflow with the EventLoop and returns a {Phronomy::Task}
|
|
90
|
+
# immediately without blocking the caller. The task resolves with the final
|
|
91
|
+
# context when the workflow finishes.
|
|
92
|
+
#
|
|
93
|
+
# This is the EventLoop-driven equivalent of spawning a thread around
|
|
94
|
+
# {#invoke}. No extra OS thread is created; the EventLoop's existing thread
|
|
95
|
+
# drives the execution.
|
|
96
|
+
#
|
|
97
|
+
# @param input [Hash] initial context field values
|
|
98
|
+
# @param config [Hash]
|
|
99
|
+
# @return [Phronomy::Task]
|
|
100
|
+
# @api private
|
|
101
|
+
def invoke_deferred(input, config: {})
|
|
102
|
+
state, thread_id, recursion_limit, store = _build_initial_context(input, config)
|
|
103
|
+
result_task = Phronomy::Task.deferred(name: "workflow-async:#{thread_id}")
|
|
104
|
+
Phronomy::EventLoop.instance.start
|
|
105
|
+
session = build_session_for(context: state, recursion_limit: recursion_limit)
|
|
106
|
+
if store && config[:thread_id]
|
|
107
|
+
# Wrap so that state is persisted when the task resolves.
|
|
108
|
+
persist_task = Phronomy::Task.deferred(name: "workflow-async-persist:#{thread_id}")
|
|
109
|
+
Phronomy::EventLoop.instance.register(session, completion: persist_task)
|
|
110
|
+
persist_task.on_complete do |result, error|
|
|
111
|
+
store.save(thread_id, {fields: result.to_h, phase: result.phase.to_s}) unless error
|
|
112
|
+
if error
|
|
113
|
+
result_task.backend.unblock(nil, error)
|
|
114
|
+
result_task.transition!(:failed, error: error)
|
|
115
|
+
else
|
|
116
|
+
result_task.backend.unblock(result, nil)
|
|
117
|
+
result_task.transition!(:completed, value: result)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
Phronomy::EventLoop.instance.register(session, completion: result_task)
|
|
122
|
+
end
|
|
123
|
+
result_task
|
|
124
|
+
end
|
|
125
|
+
|
|
105
126
|
# Generic resume. Equivalent to +send_event(state:, event: :resume, input:)+.
|
|
106
127
|
# @param state [Object] halted context
|
|
107
128
|
# @param input [Hash, nil] optional field updates to merge before resuming
|
|
@@ -142,13 +163,9 @@ module Phronomy
|
|
|
142
163
|
event
|
|
143
164
|
end
|
|
144
165
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
resume_event: ev_to_fire, resume_phase: current_phase)
|
|
149
|
-
else
|
|
150
|
-
run_workflow(state, resume_event: ev_to_fire, resume_phase: current_phase)
|
|
151
|
-
end
|
|
166
|
+
run_via_event_loop(state,
|
|
167
|
+
recursion_limit: Phronomy.configuration.recursion_limit,
|
|
168
|
+
resume_event: ev_to_fire, resume_phase: current_phase)
|
|
152
169
|
end
|
|
153
170
|
|
|
154
171
|
# Streaming execution. Yields { state: Symbol, context: Object } after each state action completes.
|
|
@@ -167,9 +184,26 @@ module Phronomy
|
|
|
167
184
|
|
|
168
185
|
private
|
|
169
186
|
|
|
187
|
+
# Builds the initial WorkflowContext from input and config.
|
|
188
|
+
# Returns [state, thread_id, recursion_limit, store].
|
|
189
|
+
def _build_initial_context(input, config)
|
|
190
|
+
thread_id = config[:thread_id] || SecureRandom.uuid
|
|
191
|
+
recursion_limit = config.fetch(:recursion_limit, Phronomy.configuration.recursion_limit)
|
|
192
|
+
store = config.fetch(:state_store, @state_store) || Phronomy.configuration.state_store
|
|
193
|
+
snapshot = (store && config[:thread_id]) ? store.load(thread_id) : nil
|
|
194
|
+
initial_fields = if snapshot && snapshot[:fields]
|
|
195
|
+
snapshot[:fields].transform_keys(&:to_sym).merge(input.transform_keys(&:to_sym))
|
|
196
|
+
else
|
|
197
|
+
input
|
|
198
|
+
end
|
|
199
|
+
state = @state_class.new(**initial_fields)
|
|
200
|
+
state.set_graph_metadata(thread_id: thread_id)
|
|
201
|
+
[state, thread_id, recursion_limit, store]
|
|
202
|
+
end
|
|
203
|
+
|
|
170
204
|
# Builds an FSMSession for the given context. Used in EventLoop mode.
|
|
171
205
|
def build_session_for(context:, recursion_limit:, resume_event: nil, resume_phase: nil)
|
|
172
|
-
Phronomy::
|
|
206
|
+
Phronomy::FSMSession.new(
|
|
173
207
|
id: context.thread_id,
|
|
174
208
|
context: context,
|
|
175
209
|
entry_point: @entry_point,
|
|
@@ -190,6 +224,9 @@ module Phronomy
|
|
|
190
224
|
# Blocks the calling thread on a completion queue until the workflow
|
|
191
225
|
# finishes, halts at a wait state, or raises an error.
|
|
192
226
|
def run_via_event_loop(context, recursion_limit:, resume_event: nil, resume_phase: nil)
|
|
227
|
+
# Ensure EventLoop is running. In tests, reset_runtime! resets the
|
|
228
|
+
# singleton without restarting it; start is idempotent when already alive.
|
|
229
|
+
Phronomy::EventLoop.instance.start
|
|
193
230
|
session = build_session_for(
|
|
194
231
|
context: context, recursion_limit: recursion_limit,
|
|
195
232
|
resume_event: resume_event, resume_phase: resume_phase
|
|
@@ -201,6 +238,19 @@ module Phronomy
|
|
|
201
238
|
end
|
|
202
239
|
|
|
203
240
|
def run_workflow(ctx, resume_event: nil, resume_phase: nil, recursion_limit: 25, &event_block)
|
|
241
|
+
# Mark the current thread as a synchronous execution context.
|
|
242
|
+
# This allows WorkflowContext field mutations via setters without raising
|
|
243
|
+
# WorkflowContextOwnershipError (which would otherwise fire since EventLoop
|
|
244
|
+
# is always active and run_workflow runs on the caller's thread).
|
|
245
|
+
Thread.current[:phronomy_sync_execution] = Thread.current[:phronomy_sync_execution].to_i + 1
|
|
246
|
+
_run_workflow_body(ctx, resume_event: resume_event, resume_phase: resume_phase,
|
|
247
|
+
recursion_limit: recursion_limit, &event_block)
|
|
248
|
+
ensure
|
|
249
|
+
depth = Thread.current[:phronomy_sync_execution].to_i - 1
|
|
250
|
+
Thread.current[:phronomy_sync_execution] = (depth > 0) ? depth : nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def _run_workflow_body(ctx, resume_event: nil, resume_phase: nil, recursion_limit: 25, &event_block)
|
|
204
254
|
if resume_event
|
|
205
255
|
# -- Resume from a wait state -------------------------------------------
|
|
206
256
|
# Fire the external event on a tracker positioned at the wait state.
|
|
@@ -231,7 +281,7 @@ module Phronomy
|
|
|
231
281
|
"Action in state #{current_state.inspect} timed out after #{timeout_secs}s"
|
|
232
282
|
end
|
|
233
283
|
end
|
|
234
|
-
task_result = result.
|
|
284
|
+
task_result = result.wait_result
|
|
235
285
|
ctx = task_result if task_result.is_a?(Phronomy::WorkflowContext)
|
|
236
286
|
elsif result.is_a?(Phronomy::WorkflowContext)
|
|
237
287
|
ctx = result
|
data/lib/phronomy.rb
CHANGED
|
@@ -11,11 +11,18 @@ loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
|
|
|
11
11
|
# RAG: Zeitwerk would infer "Rag" — override to "RAG".
|
|
12
12
|
loader.inflector.inflect("rag" => "RAG")
|
|
13
13
|
# FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
|
|
14
|
+
# Phronomy::FSMSession is the top-level cooperative execution engine shared by
|
|
15
|
+
# WorkflowRunner and Agent::InvocationSession.
|
|
14
16
|
loader.inflector.inflect("fsm_session" => "FSMSession")
|
|
15
17
|
# LLMAdapter: Zeitwerk would infer "LlmAdapter" — override to "LLMAdapter".
|
|
16
18
|
loader.inflector.inflect("llm_adapter" => "LLMAdapter")
|
|
17
19
|
# LLMAdapter::RubyLLM: "ruby_llm" maps to "RubyLLM" (not "RubyLlm").
|
|
18
20
|
loader.inflector.inflect("ruby_llm" => "RubyLLM")
|
|
21
|
+
# Collapse engine/ so that its contents autoload directly under Phronomy::
|
|
22
|
+
# (no Engine:: prefix). e.g. engine/event_loop.rb => Phronomy::EventLoop.
|
|
23
|
+
# This allows the execution engine to be organised in its own subdirectory
|
|
24
|
+
# without changing any class names or callers.
|
|
25
|
+
loader.collapse("#{__dir__}/phronomy/engine")
|
|
19
26
|
loader.setup
|
|
20
27
|
|
|
21
28
|
require_relative "phronomy/version"
|
|
@@ -87,23 +94,18 @@ module Phronomy
|
|
|
87
94
|
end
|
|
88
95
|
end
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
# Raised by a {Phronomy::Filter::Base} subclass when the filter rejects a
|
|
98
|
+
# value without transforming it (blocking the pipeline).
|
|
99
|
+
# @api public
|
|
100
|
+
class FilterBlockError < Error
|
|
101
|
+
attr_reader :filter
|
|
92
102
|
|
|
93
|
-
def initialize(message,
|
|
103
|
+
def initialize(message, filter: nil)
|
|
94
104
|
super(message)
|
|
95
|
-
@
|
|
105
|
+
@filter = filter
|
|
96
106
|
end
|
|
97
107
|
end
|
|
98
108
|
|
|
99
|
-
# Raised when {Agent::Base#resume} (or the class-level equivalent) is called
|
|
100
|
-
# with a {Agent::Checkpoint} whose +checkpoint_id+ has already been consumed
|
|
101
|
-
# by a previous +resume+ call on the same store.
|
|
102
|
-
#
|
|
103
|
-
# This protects against duplicate resume executions caused by webhook retries
|
|
104
|
-
# or queue message redelivery.
|
|
105
|
-
class CheckpointAlreadyResumedError < Error; end
|
|
106
|
-
|
|
107
109
|
# Raised when an operation is submitted to a {BlockingAdapterPool} that has
|
|
108
110
|
# already been shut down via {BlockingAdapterPool#shutdown}.
|
|
109
111
|
class PoolShutdownError < Error; end
|
|
@@ -184,8 +186,17 @@ module Phronomy
|
|
|
184
186
|
# config.around { |ex| Phronomy.reset_runtime! ; ex.run ; Phronomy.reset_runtime! }
|
|
185
187
|
# @api public
|
|
186
188
|
def reset_runtime!
|
|
187
|
-
|
|
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
|
|
188
198
|
@configuration = Configuration.new
|
|
199
|
+
@configuration.event_loop_stop_grace_seconds = prev_grace if prev_grace
|
|
189
200
|
end
|
|
190
201
|
end
|
|
191
202
|
end
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -32,8 +32,8 @@ PUBLIC_API_ENTRIES = [
|
|
|
32
32
|
# Beta
|
|
33
33
|
Phronomy::MultiAgent::Orchestrator,
|
|
34
34
|
Phronomy::MultiAgent::TeamCoordinator,
|
|
35
|
-
Phronomy::
|
|
36
|
-
Phronomy::
|
|
35
|
+
Phronomy::Filter::Base,
|
|
36
|
+
Phronomy::Filter::PromptInjectionFilter,
|
|
37
37
|
Phronomy::VectorStore::Base,
|
|
38
38
|
Phronomy::VectorStore::InMemory,
|
|
39
39
|
Phronomy::VectorStore::Embeddings::Base,
|
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.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
|
|
@@ -64,9 +64,9 @@ dependencies:
|
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0.6'
|
|
67
|
-
description: Phronomy
|
|
68
|
-
|
|
69
|
-
LLM abstraction.
|
|
67
|
+
description: Phronomy is a Ruby AI agent framework that provides composable building
|
|
68
|
+
blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
|
|
69
|
+
in Ruby. Powered by RubyLLM for LLM abstraction.
|
|
70
70
|
email:
|
|
71
71
|
- raizo.tcs@gmail.com
|
|
72
72
|
executables: []
|
|
@@ -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
|
-
- lib/phronomy/agent/concerns/
|
|
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,13 +166,10 @@ 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/
|
|
169
|
+
- lib/phronomy/filter.rb
|
|
170
|
+
- lib/phronomy/filter/base.rb
|
|
171
|
+
- lib/phronomy/filter/prompt_injection_filter.rb
|
|
150
172
|
- lib/phronomy/generator_verifier.rb
|
|
151
|
-
- lib/phronomy/guardrail.rb
|
|
152
|
-
- lib/phronomy/guardrail/base.rb
|
|
153
|
-
- lib/phronomy/guardrail/input_guardrail.rb
|
|
154
|
-
- lib/phronomy/guardrail/output_guardrail.rb
|
|
155
|
-
- lib/phronomy/guardrail/prompt_injection_guardrail.rb
|
|
156
173
|
- lib/phronomy/invocation_context.rb
|
|
157
174
|
- lib/phronomy/knowledge_source.rb
|
|
158
175
|
- lib/phronomy/llm_adapter.rb
|
|
@@ -173,31 +190,13 @@ files:
|
|
|
173
190
|
- lib/phronomy/output_parser/structured_parser.rb
|
|
174
191
|
- lib/phronomy/ruby_llm_patches.rb
|
|
175
192
|
- lib/phronomy/runnable.rb
|
|
176
|
-
- lib/phronomy/runtime.rb
|
|
177
|
-
- lib/phronomy/runtime/deterministic_scheduler.rb
|
|
178
|
-
- lib/phronomy/runtime/fake_scheduler.rb
|
|
179
|
-
- lib/phronomy/runtime/runtime_metrics.rb
|
|
180
|
-
- lib/phronomy/runtime/scheduler.rb
|
|
181
|
-
- lib/phronomy/runtime/scheduler_timer_adapter.rb
|
|
182
|
-
- lib/phronomy/runtime/task_registry.rb
|
|
183
|
-
- lib/phronomy/runtime/thread_scheduler.rb
|
|
184
|
-
- lib/phronomy/runtime/timer_queue.rb
|
|
185
|
-
- lib/phronomy/runtime/timer_service.rb
|
|
186
193
|
- lib/phronomy/state_store/base.rb
|
|
187
194
|
- lib/phronomy/state_store/in_memory.rb
|
|
188
|
-
- lib/phronomy/task.rb
|
|
189
|
-
- lib/phronomy/task/backend.rb
|
|
190
|
-
- lib/phronomy/task/fiber_backend.rb
|
|
191
|
-
- lib/phronomy/task/immediate_backend.rb
|
|
192
|
-
- lib/phronomy/task/mapped_backend.rb
|
|
193
|
-
- lib/phronomy/task/thread_backend.rb
|
|
194
|
-
- lib/phronomy/task_group.rb
|
|
195
195
|
- lib/phronomy/testing.rb
|
|
196
196
|
- lib/phronomy/testing/fake_clock.rb
|
|
197
197
|
- lib/phronomy/testing/fake_scheduler.rb
|
|
198
198
|
- lib/phronomy/testing/scheduler_helpers.rb
|
|
199
199
|
- lib/phronomy/token_usage.rb
|
|
200
|
-
- lib/phronomy/tool.rb
|
|
201
200
|
- lib/phronomy/tools/agent.rb
|
|
202
201
|
- lib/phronomy/tools/mcp.rb
|
|
203
202
|
- lib/phronomy/tools/vector_search.rb
|
|
@@ -223,7 +222,6 @@ files:
|
|
|
223
222
|
- lib/phronomy/vector_store/splitter/recursive_splitter.rb
|
|
224
223
|
- lib/phronomy/version.rb
|
|
225
224
|
- lib/phronomy/workflow.rb
|
|
226
|
-
- lib/phronomy/workflow/fsm_session.rb
|
|
227
225
|
- lib/phronomy/workflow/phase_machine_builder.rb
|
|
228
226
|
- lib/phronomy/workflow_context.rb
|
|
229
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
|