phronomy 0.7.1 → 0.9.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/README.md +35 -45
- data/benchmark/baseline.json +1 -1
- data/benchmark/bench_agent_invoke.rb +1 -1
- data/benchmark/bench_context_assembler.rb +11 -3
- data/benchmark/bench_regression.rb +11 -11
- data/benchmark/bench_token_estimator.rb +5 -5
- data/benchmark/bench_tool_schema.rb +2 -2
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +224 -0
- data/lib/phronomy/agent/base.rb +268 -403
- data/lib/phronomy/agent/checkpoint.rb +118 -0
- data/lib/phronomy/agent/concerns/suspendable.rb +6 -6
- data/lib/phronomy/agent/context/capability/base.rb +689 -0
- data/lib/phronomy/agent/context/capability/scope_policy.rb +54 -0
- data/lib/phronomy/agent/context/instruction/prompt_template.rb +102 -0
- data/lib/phronomy/agent/context/knowledge/base.rb +58 -0
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +102 -0
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +58 -0
- data/lib/phronomy/agent/fsm.rb +1 -1
- data/lib/phronomy/agent/invocation_pipeline.rb +108 -0
- data/lib/phronomy/agent/lifecycle/fsm_session.rb +251 -0
- data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +249 -0
- data/lib/phronomy/agent/react_agent.rb +43 -37
- data/lib/phronomy/agent/runner.rb +2 -2
- data/lib/phronomy/agent/shared_state.rb +2 -2
- data/lib/phronomy/agent/tool_executor.rb +108 -0
- data/lib/phronomy/concurrency/async_queue.rb +157 -0
- data/lib/phronomy/concurrency/blocking_adapter_pool.rb +443 -0
- data/lib/phronomy/concurrency/cancellation_scope.rb +125 -0
- data/lib/phronomy/concurrency/cancellation_token.rb +140 -0
- data/lib/phronomy/concurrency/concurrency_gate.rb +157 -0
- data/lib/phronomy/concurrency/deadline.rb +65 -0
- data/lib/phronomy/{runtime → concurrency}/gate_registry.rb +1 -2
- data/lib/phronomy/{runtime → concurrency}/pool_registry.rb +1 -1
- data/lib/phronomy/configuration.rb +0 -6
- data/lib/phronomy/context.rb +2 -8
- data/lib/phronomy/eval/runner.rb +4 -0
- data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
- data/lib/phronomy/event_loop.rb +7 -7
- data/lib/phronomy/invocation_context.rb +3 -3
- data/lib/phronomy/knowledge_source.rb +0 -5
- data/lib/phronomy/llm_adapter/ruby_llm.rb +17 -11
- data/lib/phronomy/llm_context_window/assembler.rb +191 -0
- data/lib/phronomy/{context → llm_context_window}/context_version_cache.rb +1 -1
- data/lib/phronomy/{context → llm_context_window}/token_budget.rb +7 -4
- data/lib/phronomy/{context → llm_context_window}/token_estimator.rb +3 -3
- data/lib/phronomy/{agent → multi_agent}/handoff.rb +6 -6
- data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +7 -7
- data/lib/phronomy/{agent → multi_agent}/parallel_tool_chat.rb +4 -4
- data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +4 -4
- data/lib/phronomy/runtime/runtime_metrics.rb +0 -1
- data/lib/phronomy/runtime.rb +20 -6
- data/lib/phronomy/task_group.rb +1 -1
- data/lib/phronomy/tool.rb +3 -4
- data/lib/phronomy/{tool/agent_tool.rb → tools/agent.rb} +6 -6
- data/lib/phronomy/{tool/mcp_tool.rb → tools/mcp.rb} +9 -9
- data/lib/phronomy/tools/vector_search.rb +70 -0
- data/lib/phronomy/tracing/null_tracer.rb +3 -1
- data/lib/phronomy/vector_store/async_backend.rb +4 -4
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +41 -0
- data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +47 -0
- data/lib/phronomy/vector_store/in_memory.rb +12 -2
- data/lib/phronomy/vector_store/loader/base.rb +27 -0
- data/lib/phronomy/vector_store/loader/csv_loader.rb +58 -0
- data/lib/phronomy/vector_store/loader/markdown_loader.rb +78 -0
- data/lib/phronomy/vector_store/loader/plain_text_loader.rb +24 -0
- data/lib/phronomy/vector_store/pgvector.rb +2 -2
- data/lib/phronomy/vector_store/redis_search.rb +2 -2
- data/lib/phronomy/vector_store/splitter/base.rb +49 -0
- data/lib/phronomy/vector_store/splitter/fixed_size_splitter.rb +53 -0
- data/lib/phronomy/vector_store/splitter/recursive_splitter.rb +107 -0
- data/lib/phronomy/vector_store.rb +14 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_context.rb +8 -0
- data/lib/phronomy/workflow_runner.rb +11 -131
- data/lib/phronomy.rb +2 -0
- data/scripts/api_snapshot.rb +11 -9
- metadata +44 -46
- data/lib/phronomy/async_queue.rb +0 -155
- data/lib/phronomy/blocking_adapter_pool.rb +0 -435
- data/lib/phronomy/cancellation_scope.rb +0 -123
- data/lib/phronomy/cancellation_token.rb +0 -133
- data/lib/phronomy/concurrency_gate.rb +0 -155
- data/lib/phronomy/context/assembler.rb +0 -143
- data/lib/phronomy/context/compaction_context.rb +0 -111
- data/lib/phronomy/context/trigger_context.rb +0 -39
- data/lib/phronomy/context/trim_context.rb +0 -75
- data/lib/phronomy/deadline.rb +0 -63
- data/lib/phronomy/embeddings/base.rb +0 -39
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
- data/lib/phronomy/embeddings.rb +0 -11
- data/lib/phronomy/fsm_session.rb +0 -247
- data/lib/phronomy/knowledge_source/base.rb +0 -54
- data/lib/phronomy/knowledge_source/entity_knowledge.rb +0 -96
- data/lib/phronomy/knowledge_source/rag_knowledge.rb +0 -57
- data/lib/phronomy/knowledge_source/static_knowledge.rb +0 -52
- data/lib/phronomy/loader/base.rb +0 -25
- data/lib/phronomy/loader/csv_loader.rb +0 -56
- data/lib/phronomy/loader/markdown_loader.rb +0 -76
- data/lib/phronomy/loader/plain_text_loader.rb +0 -22
- data/lib/phronomy/loader.rb +0 -13
- data/lib/phronomy/prompt_template.rb +0 -96
- data/lib/phronomy/splitter/base.rb +0 -47
- data/lib/phronomy/splitter/fixed_size_splitter.rb +0 -51
- data/lib/phronomy/splitter/recursive_splitter.rb +0 -105
- data/lib/phronomy/splitter.rb +0 -12
- data/lib/phronomy/tool/base.rb +0 -644
- data/lib/phronomy/tool/scope_policy.rb +0 -50
- data/lib/phronomy/tool_executor.rb +0 -106
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module VectorStore
|
|
5
|
+
module Splitter
|
|
6
|
+
# Splits text recursively using a prioritised list of separator strings.
|
|
7
|
+
#
|
|
8
|
+
# The splitter tries each separator in order. When a separator produces
|
|
9
|
+
# chunks that are still larger than +chunk_size+, it recurses with the
|
|
10
|
+
# next separator in the list. This mirrors LangChain's
|
|
11
|
+
# RecursiveCharacterTextSplitter behaviour.
|
|
12
|
+
#
|
|
13
|
+
# Default separators (in priority order):
|
|
14
|
+
# 1. "\n\n" — paragraph breaks
|
|
15
|
+
# 2. "\n" — line breaks
|
|
16
|
+
# 3. ". " — sentence boundaries
|
|
17
|
+
# 4. " " — word boundaries
|
|
18
|
+
# 5. "" — character-level fallback
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# splitter = Phronomy::VectorStore::Splitter::RecursiveSplitter.new(chunk_size: 300, chunk_overlap: 30)
|
|
22
|
+
# chunks = splitter.split({ text: long_markdown, metadata: { source: "guide.md" } })
|
|
23
|
+
class RecursiveSplitter < Base
|
|
24
|
+
DEFAULT_SEPARATORS = ["\n\n", "\n", ". ", " ", ""].freeze
|
|
25
|
+
|
|
26
|
+
# @param chunk_size [Integer] maximum characters per chunk (default: 1000)
|
|
27
|
+
# @param chunk_overlap [Integer] overlap characters (default: 200)
|
|
28
|
+
# @param separators [Array<String>] separator list in priority order
|
|
29
|
+
# @api public
|
|
30
|
+
def initialize(chunk_size: 1000, chunk_overlap: 200, separators: DEFAULT_SEPARATORS)
|
|
31
|
+
raise ArgumentError, "chunk_overlap must be less than chunk_size" if chunk_overlap >= chunk_size
|
|
32
|
+
|
|
33
|
+
@chunk_size = chunk_size
|
|
34
|
+
@chunk_overlap = chunk_overlap
|
|
35
|
+
@separators = separators
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param document [Hash, String]
|
|
39
|
+
# @return [Array<Hash>]
|
|
40
|
+
# @api public
|
|
41
|
+
def split(document)
|
|
42
|
+
doc = normalise(document)
|
|
43
|
+
texts = recursive_split(doc[:text], @separators)
|
|
44
|
+
merge_with_overlap(texts).each_with_index.map do |text, idx|
|
|
45
|
+
{text: text, metadata: doc[:metadata].merge(chunk: idx)}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Split +text+ using the first separator that yields non-trivial pieces,
|
|
52
|
+
# then recurse on any piece that is still too large.
|
|
53
|
+
def recursive_split(text, separators)
|
|
54
|
+
return [text] if text.length <= @chunk_size || separators.empty?
|
|
55
|
+
|
|
56
|
+
sep, *rest_seps = separators
|
|
57
|
+
|
|
58
|
+
# Character-level fallback: just slice
|
|
59
|
+
if sep == ""
|
|
60
|
+
return FixedSizeSplitter
|
|
61
|
+
.new(chunk_size: @chunk_size, chunk_overlap: @chunk_overlap)
|
|
62
|
+
.split(text)
|
|
63
|
+
.map { |c| c[:text] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
parts = text.split(sep)
|
|
67
|
+
|
|
68
|
+
# If this separator doesn't split, try the next
|
|
69
|
+
return recursive_split(text, rest_seps) if parts.length <= 1
|
|
70
|
+
|
|
71
|
+
# Re-attach the separator to each part except the last so context is preserved
|
|
72
|
+
parts_with_sep = parts.each_with_index.map do |part, i|
|
|
73
|
+
(i < parts.length - 1) ? part + sep : part
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
parts_with_sep.flat_map do |part|
|
|
77
|
+
if part.length > @chunk_size
|
|
78
|
+
recursive_split(part, rest_seps)
|
|
79
|
+
else
|
|
80
|
+
[part]
|
|
81
|
+
end
|
|
82
|
+
end.reject { |t| t.strip.empty? }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Merge small adjacent pieces and apply overlap between chunks.
|
|
86
|
+
def merge_with_overlap(texts)
|
|
87
|
+
merged = []
|
|
88
|
+
current = +""
|
|
89
|
+
|
|
90
|
+
texts.each do |text|
|
|
91
|
+
if current.length + text.length <= @chunk_size
|
|
92
|
+
current << text
|
|
93
|
+
else
|
|
94
|
+
merged << current.strip unless current.strip.empty?
|
|
95
|
+
# Start next chunk with overlap from the end of current
|
|
96
|
+
overlap_text = (current.length > @chunk_overlap) ? current[-@chunk_overlap..] : current
|
|
97
|
+
current = overlap_text + text
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
merged << current.strip unless current.strip.empty?
|
|
102
|
+
merged
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
# Vector store
|
|
4
|
+
# Vector store infrastructure: backends, embeddings adapters, document loaders,
|
|
5
|
+
# and text splitters.
|
|
5
6
|
#
|
|
6
|
-
# Sub-
|
|
7
|
+
# Sub-namespaces are auto-loaded by Zeitwerk:
|
|
7
8
|
# Phronomy::VectorStore::Base
|
|
8
9
|
# Phronomy::VectorStore::InMemory
|
|
10
|
+
# Phronomy::VectorStore::Pgvector
|
|
11
|
+
# Phronomy::VectorStore::RedisSearch
|
|
12
|
+
# Phronomy::VectorStore::Embeddings::Base
|
|
13
|
+
# Phronomy::VectorStore::Embeddings::RubyLLMEmbeddings
|
|
14
|
+
# Phronomy::VectorStore::Loader::Base
|
|
15
|
+
# Phronomy::VectorStore::Loader::PlainTextLoader
|
|
16
|
+
# Phronomy::VectorStore::Loader::MarkdownLoader
|
|
17
|
+
# Phronomy::VectorStore::Loader::CsvLoader
|
|
18
|
+
# Phronomy::VectorStore::Splitter::Base
|
|
19
|
+
# Phronomy::VectorStore::Splitter::FixedSizeSplitter
|
|
20
|
+
# Phronomy::VectorStore::Splitter::RecursiveSplitter
|
|
9
21
|
module VectorStore
|
|
10
22
|
end
|
|
11
23
|
end
|
data/lib/phronomy/version.rb
CHANGED
|
@@ -70,6 +70,7 @@ module Phronomy
|
|
|
70
70
|
# :<state> — resuming at <state> (workflow paused before its execution)
|
|
71
71
|
# @return [Symbol]
|
|
72
72
|
# @api public
|
|
73
|
+
# mutant:disable - @phase is always non-nil (set to :__end__ in initialize, only changed by set_graph_metadata which never sets nil), so the || :__end__ fallback branch is never reached — all mutations of the right-hand side are genuine equivalents
|
|
73
74
|
def phase
|
|
74
75
|
@phase || :__end__
|
|
75
76
|
end
|
|
@@ -77,6 +78,7 @@ module Phronomy
|
|
|
77
78
|
# Returns true if the workflow is paused mid-execution (not yet completed).
|
|
78
79
|
# @return [Boolean]
|
|
79
80
|
# @api public
|
|
81
|
+
# mutant:disable - phase != :__end__ vs !phase.eql?(:__end__) vs !phase.equal?(:__end__) are genuine equivalents for Symbol (Symbols are interned so == / eql? / equal? all behave identically)
|
|
80
82
|
def halted?
|
|
81
83
|
phase != :__end__
|
|
82
84
|
end
|
|
@@ -85,12 +87,14 @@ module Phronomy
|
|
|
85
87
|
# @param thread_id [String, nil]
|
|
86
88
|
# @param phase [Symbol, nil]
|
|
87
89
|
# @api public
|
|
90
|
+
# mutant:disable - mutations replacing return value `self` with nil or removing the last line are genuine equivalents: callers chain on the return value only in merge which immediately discards it
|
|
88
91
|
def set_graph_metadata(thread_id: nil, phase: nil)
|
|
89
92
|
@thread_id = thread_id unless thread_id.nil?
|
|
90
93
|
@phase = phase unless phase.nil?
|
|
91
94
|
self
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
# mutant:disable - multiple genuine equivalent mutations: is_a?(Proc) vs instance_of?(Proc) (Proc has no subclasses in practice), config[]/fetch() for always-present :default key, @thread_id=nil removal (unset ivar is already nil), @phase=:__end__ → nil or removal (phase method returns :__end__ via @phase||:__end__ fallback), raise message #{.inspect} vs #{} (spec checks exception class not message text)
|
|
94
98
|
def initialize(**attrs)
|
|
95
99
|
unknown = attrs.keys - self.class.fields.keys
|
|
96
100
|
raise ArgumentError, "Unknown WorkflowContext field(s): #{unknown.inspect}" unless unknown.empty?
|
|
@@ -114,6 +118,7 @@ module Phronomy
|
|
|
114
118
|
# @return [self.class] new context instance
|
|
115
119
|
# @raise [ArgumentError] if updates contains keys that are not declared fields
|
|
116
120
|
# @api public
|
|
121
|
+
# mutant:disable - multiple genuine equivalent mutations: send/public_send/__send__ are identical (all field accessors are public), fields[]/fetch() and field_config[]/fetch() for always-present keys, updates[]/fetch() when updates.key?(name) is already true, Array() wrapping for append fields that always hold Arrays, (send||{})/send equivalence for merge fields that always hold Hashes, deep_dup_value(send) vs send are equivalent under killfork (coverage selection does not trace the deep_dup_value call site across the fork boundary), raise message inspect vs to_s (spec checks exception class only)
|
|
117
122
|
def merge(updates)
|
|
118
123
|
unknown = updates.keys - self.class.fields.keys
|
|
119
124
|
raise ArgumentError, "Unknown WorkflowContext field(s): #{unknown.inspect}" unless unknown.empty?
|
|
@@ -145,6 +150,7 @@ module Phronomy
|
|
|
145
150
|
# Converts user-defined fields to a Hash (excludes internal workflow metadata).
|
|
146
151
|
# @return [Hash]
|
|
147
152
|
# @api public
|
|
153
|
+
# mutant:disable - send/public_send/__send__ are genuine equivalents (all field accessors are public methods)
|
|
148
154
|
def to_h
|
|
149
155
|
self.class.fields.keys.each_with_object({}) do |name, h|
|
|
150
156
|
h[name] = send(name)
|
|
@@ -158,6 +164,7 @@ module Phronomy
|
|
|
158
164
|
# @raise [Phronomy::WorkflowContextOwnershipError] when called from a
|
|
159
165
|
# non-EventLoop thread in EventLoop mode.
|
|
160
166
|
# @api private
|
|
167
|
+
# mutant:disable - multiple genuine equivalent mutations: defined?(Phronomy::EventLoop)&& removal is genuine because EventLoop is always loaded in the killfork environment; true&& is genuine (truthy guard); EventLoop.current? resolves to Phronomy::EventLoop.current? within the Phronomy module; WorkflowContextOwnershipError resolves to Phronomy::WorkflowContextOwnershipError within the module; raise without message or with nil message is genuine (spec checks exception class, not message text)
|
|
161
168
|
def _assert_write_permitted!
|
|
162
169
|
return unless defined?(Phronomy::EventLoop) &&
|
|
163
170
|
Phronomy.configuration.event_loop
|
|
@@ -174,6 +181,7 @@ module Phronomy
|
|
|
174
181
|
# Immutable values (nil, Symbol, Integer, Float, true/false, frozen String) are returned as-is.
|
|
175
182
|
# Other objects are dup'd (best-effort shallow copy for custom types).
|
|
176
183
|
# Objects that cannot be dup'd (e.g. Proc, Method) are returned as-is.
|
|
184
|
+
# mutant:disable - multiple genuine equivalent mutations: each class in the when clause (NilClass/Symbol/Integer/Float/TrueClass/FalseClass) can be removed or replaced with nil because all those types are frozen so the else-branch val.frozen? guard returns the same result; return val vs val is also equivalent; if val.frozen? vs if self.frozen? is equivalent since self is never frozen in this context
|
|
177
185
|
def deep_dup_value(val)
|
|
178
186
|
case val
|
|
179
187
|
when Array
|
|
@@ -56,7 +56,16 @@ module Phronomy
|
|
|
56
56
|
@wait_state_names = wait_state_names
|
|
57
57
|
@state_store = state_store
|
|
58
58
|
@action_timeouts = action_timeouts # { state_name => seconds }
|
|
59
|
-
@phase_machine_class =
|
|
59
|
+
@phase_machine_class = Agent::Lifecycle::PhaseMachineBuilder.new(
|
|
60
|
+
entry_point: @entry_point,
|
|
61
|
+
declared_states: @declared_states,
|
|
62
|
+
wait_state_names: @wait_state_names,
|
|
63
|
+
external_events: @external_events,
|
|
64
|
+
entry_actions: @entry_actions,
|
|
65
|
+
action_timeouts: @action_timeouts,
|
|
66
|
+
auto_transitions: auto_transitions,
|
|
67
|
+
exit_actions: exit_actions
|
|
68
|
+
).build
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
# Executes the workflow from the initial state.
|
|
@@ -160,7 +169,7 @@ module Phronomy
|
|
|
160
169
|
|
|
161
170
|
# Builds an FSMSession for the given context. Used in EventLoop mode.
|
|
162
171
|
def build_session_for(context:, recursion_limit:, resume_event: nil, resume_phase: nil)
|
|
163
|
-
Phronomy::FSMSession.new(
|
|
172
|
+
Phronomy::Agent::Lifecycle::FSMSession.new(
|
|
164
173
|
id: context.thread_id,
|
|
165
174
|
context: context,
|
|
166
175
|
entry_point: @entry_point,
|
|
@@ -310,135 +319,6 @@ module Phronomy
|
|
|
310
319
|
# before_transition from — exit callbacks (invoked when leaving a state)
|
|
311
320
|
#
|
|
312
321
|
# Guard lambdas bridge the PhaseTracker and WorkflowContext via +m.context+.
|
|
313
|
-
def build_phase_machine_class(auto_transitions, exit_actions)
|
|
314
|
-
entry = @entry_point
|
|
315
|
-
all_states = (@declared_states + @wait_state_names + [:__end__]).uniq
|
|
316
|
-
auto_trans = auto_transitions # Array of { from:, to:, guard: }
|
|
317
|
-
ext_events = @external_events
|
|
318
|
-
entry_acts = @entry_actions
|
|
319
|
-
exit_acts = exit_actions
|
|
320
|
-
act_timeouts = @action_timeouts # { state_name => seconds }
|
|
321
|
-
|
|
322
|
-
Class.new do
|
|
323
|
-
# Holds the current WorkflowContext so guards and callbacks can read it.
|
|
324
|
-
attr_accessor :context
|
|
325
|
-
|
|
326
|
-
# Set to true by an entry action that returned an awaitable Task.
|
|
327
|
-
# When true, FSMSession skips the automatic advance_or_halt step and
|
|
328
|
-
# waits for the async worker thread to post a state_completed event back.
|
|
329
|
-
attr_accessor :async_pending
|
|
330
|
-
|
|
331
|
-
state_machine :phase, initial: entry do
|
|
332
|
-
all_states.each { |s| state s }
|
|
333
|
-
|
|
334
|
-
# Auto-fire transitions: all auto transitions unified under :state_completed.
|
|
335
|
-
# Includes unguarded (unconditional) and guarded (conditional) transitions.
|
|
336
|
-
# Declaration order is preserved; guards are evaluated before unguarded fallbacks.
|
|
337
|
-
event :state_completed do
|
|
338
|
-
auto_trans.each do |t|
|
|
339
|
-
if t[:guard]
|
|
340
|
-
guard_proc = t[:guard]
|
|
341
|
-
transition t[:from] => t[:to], :if => ->(m) { guard_proc.call(m.context) }
|
|
342
|
-
else
|
|
343
|
-
transition t[:from] => t[:to]
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# External events: human-in-the-loop triggers from wait states.
|
|
349
|
-
ext_events.each do |ev_name, transitions|
|
|
350
|
-
event ev_name do
|
|
351
|
-
transitions.each do |t|
|
|
352
|
-
if t[:guard]
|
|
353
|
-
guard_proc = t[:guard]
|
|
354
|
-
transition t[:from] => t[:to], :if => ->(m) { guard_proc.call(m.context) }
|
|
355
|
-
else
|
|
356
|
-
transition t[:from] => t[:to]
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Entry callbacks: fire after_transition into each state.
|
|
363
|
-
# Each callable is registered as a separate callback; state_machines
|
|
364
|
-
# accumulates them and fires in declaration order.
|
|
365
|
-
# If the callable returns a WorkflowContext (e.g. via s.merge(...)),
|
|
366
|
-
# the returned context replaces the current one on the tracker.
|
|
367
|
-
entry_acts.each do |state_name, callables|
|
|
368
|
-
callables.each do |callable|
|
|
369
|
-
timeout_secs = act_timeouts[state_name]
|
|
370
|
-
after_transition to: state_name do |machine|
|
|
371
|
-
result = callable.call(machine.context)
|
|
372
|
-
if result.is_a?(Phronomy::Task)
|
|
373
|
-
if Phronomy.configuration.event_loop
|
|
374
|
-
# EventLoop mode: await in a background task so the EventLoop
|
|
375
|
-
# thread is not blocked. Signal async_pending so FSMSession
|
|
376
|
-
# skips the automatic advance_or_halt step.
|
|
377
|
-
machine.async_pending = true
|
|
378
|
-
ctx_ref = machine.context
|
|
379
|
-
thread_id = ctx_ref.thread_id
|
|
380
|
-
Phronomy::Runtime.instance.spawn(name: "wf-await-#{thread_id}") do
|
|
381
|
-
if timeout_secs
|
|
382
|
-
if result.join(timeout_secs).nil?
|
|
383
|
-
result.cancel!
|
|
384
|
-
raise Phronomy::ActionTimeoutError,
|
|
385
|
-
"Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
task_result = result.await
|
|
389
|
-
if task_result.is_a?(Phronomy::WorkflowContext)
|
|
390
|
-
Phronomy::EventLoop.instance.post(
|
|
391
|
-
Phronomy::Event.new(
|
|
392
|
-
type: :action_completed,
|
|
393
|
-
target_id: thread_id,
|
|
394
|
-
payload: task_result
|
|
395
|
-
)
|
|
396
|
-
)
|
|
397
|
-
else
|
|
398
|
-
Phronomy::EventLoop.instance.post(
|
|
399
|
-
Phronomy::Event.new(type: :state_completed, target_id: thread_id, payload: nil)
|
|
400
|
-
)
|
|
401
|
-
end
|
|
402
|
-
rescue => e
|
|
403
|
-
Phronomy::EventLoop.instance.post(
|
|
404
|
-
Phronomy::Event.new(type: :error, target_id: thread_id, payload: e)
|
|
405
|
-
)
|
|
406
|
-
end
|
|
407
|
-
else
|
|
408
|
-
# Non-EventLoop mode: block synchronously on the task result.
|
|
409
|
-
if timeout_secs
|
|
410
|
-
if result.join(timeout_secs).nil?
|
|
411
|
-
result.cancel!
|
|
412
|
-
raise Phronomy::ActionTimeoutError,
|
|
413
|
-
"Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
task_result = result.await
|
|
417
|
-
machine.context = task_result if task_result.is_a?(Phronomy::WorkflowContext)
|
|
418
|
-
end
|
|
419
|
-
elsif result.is_a?(Phronomy::WorkflowContext)
|
|
420
|
-
machine.context = result
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Exit callbacks: fire before_transition out of each state.
|
|
427
|
-
# Each callable is registered as a separate callback; state_machines
|
|
428
|
-
# accumulates them and fires in declaration order.
|
|
429
|
-
exit_acts.each do |state_name, callables|
|
|
430
|
-
callables.each do |callable|
|
|
431
|
-
before_transition from: state_name do |machine|
|
|
432
|
-
callable.call(machine.context)
|
|
433
|
-
end
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
rescue => e
|
|
439
|
-
raise ArgumentError, "Failed to build phase machine: #{e.message}"
|
|
440
|
-
end
|
|
441
|
-
|
|
442
322
|
# Creates a PhaseTracker instance initialized to +from_state+.
|
|
443
323
|
def new_phase_machine(from_state)
|
|
444
324
|
machine = @phase_machine_class.new
|
data/lib/phronomy.rb
CHANGED
|
@@ -8,6 +8,8 @@ loader = Zeitwerk::Loader.for_gem
|
|
|
8
8
|
# Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
|
|
9
9
|
# ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
|
|
10
10
|
loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
|
|
11
|
+
# RAG: Zeitwerk would infer "Rag" — override to "RAG".
|
|
12
|
+
loader.inflector.inflect("rag" => "RAG")
|
|
11
13
|
# FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
|
|
12
14
|
loader.inflector.inflect("fsm_session" => "FSMSession")
|
|
13
15
|
# AgentFSM: Zeitwerk would infer "Fsm" — override to "FSM".
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -24,26 +24,28 @@ require_relative "../lib/phronomy"
|
|
|
24
24
|
PUBLIC_API_ENTRIES = [
|
|
25
25
|
# Stable
|
|
26
26
|
Phronomy::Agent::Base,
|
|
27
|
-
Phronomy::
|
|
27
|
+
Phronomy::Agent::Context::Capability::Base,
|
|
28
28
|
Phronomy::Workflow,
|
|
29
29
|
Phronomy::WorkflowContext,
|
|
30
30
|
Phronomy::Runnable,
|
|
31
|
-
Phronomy::PromptTemplate,
|
|
31
|
+
Phronomy::Agent::Context::Instruction::PromptTemplate,
|
|
32
32
|
# Beta
|
|
33
33
|
Phronomy::Agent::ReactAgent,
|
|
34
|
-
Phronomy::
|
|
35
|
-
Phronomy::
|
|
34
|
+
Phronomy::MultiAgent::Orchestrator,
|
|
35
|
+
Phronomy::MultiAgent::TeamCoordinator,
|
|
36
36
|
Phronomy::Guardrail::InputGuardrail,
|
|
37
37
|
Phronomy::Guardrail::OutputGuardrail,
|
|
38
38
|
Phronomy::VectorStore::Base,
|
|
39
39
|
Phronomy::VectorStore::InMemory,
|
|
40
|
-
Phronomy::Embeddings::Base,
|
|
41
|
-
Phronomy::
|
|
42
|
-
Phronomy::
|
|
43
|
-
Phronomy::KnowledgeSource::RAGKnowledge,
|
|
40
|
+
Phronomy::VectorStore::Embeddings::Base,
|
|
41
|
+
Phronomy::Agent::Context::Knowledge::Base,
|
|
42
|
+
Phronomy::Agent::Context::Knowledge::StaticKnowledge,
|
|
44
43
|
Phronomy::Tracing::Base,
|
|
45
44
|
Phronomy::Tracing::NullTracer,
|
|
46
|
-
Phronomy::Eval::Runner
|
|
45
|
+
Phronomy::Eval::Runner,
|
|
46
|
+
Phronomy::Tools::Mcp,
|
|
47
|
+
Phronomy::Tools::Agent,
|
|
48
|
+
Phronomy::Tools::VectorSearch
|
|
47
49
|
].freeze
|
|
48
50
|
|
|
49
51
|
# Baseline methods common to all Ruby objects — excluded from the snapshot.
|
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.9.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-06-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -64,8 +64,9 @@ dependencies:
|
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0.6'
|
|
67
|
-
description: Phronomy provides
|
|
68
|
-
|
|
67
|
+
description: Phronomy provides composable building blocks — Agents, Workflows, Tools,
|
|
68
|
+
Guardrails, and Tracing — for building AI agents in Ruby. Powered by RubyLLM for
|
|
69
|
+
LLM abstraction.
|
|
69
70
|
email:
|
|
70
71
|
- raizo.tcs@gmail.com
|
|
71
72
|
executables: []
|
|
@@ -99,6 +100,7 @@ files:
|
|
|
99
100
|
- docs/decisions/008-orchestrator-uses-os-threads.md
|
|
100
101
|
- docs/decisions/009-state-store-abstraction.md
|
|
101
102
|
- docs/decisions/010-cooperative-first-concurrency.md
|
|
103
|
+
- docs/decisions/011-build-context-as-single-llm-input-authority.md
|
|
102
104
|
- lib/phronomy.rb
|
|
103
105
|
- lib/phronomy/agent.rb
|
|
104
106
|
- lib/phronomy/agent/base.rb
|
|
@@ -109,34 +111,32 @@ files:
|
|
|
109
111
|
- lib/phronomy/agent/concerns/guardrailable.rb
|
|
110
112
|
- lib/phronomy/agent/concerns/retryable.rb
|
|
111
113
|
- lib/phronomy/agent/concerns/suspendable.rb
|
|
114
|
+
- lib/phronomy/agent/context/capability/base.rb
|
|
115
|
+
- lib/phronomy/agent/context/capability/scope_policy.rb
|
|
116
|
+
- lib/phronomy/agent/context/instruction/prompt_template.rb
|
|
117
|
+
- lib/phronomy/agent/context/knowledge/base.rb
|
|
118
|
+
- lib/phronomy/agent/context/knowledge/entity_knowledge.rb
|
|
119
|
+
- lib/phronomy/agent/context/knowledge/static_knowledge.rb
|
|
112
120
|
- lib/phronomy/agent/fsm.rb
|
|
113
|
-
- lib/phronomy/agent/
|
|
114
|
-
- lib/phronomy/agent/
|
|
115
|
-
- lib/phronomy/agent/
|
|
121
|
+
- lib/phronomy/agent/invocation_pipeline.rb
|
|
122
|
+
- lib/phronomy/agent/lifecycle/fsm_session.rb
|
|
123
|
+
- lib/phronomy/agent/lifecycle/phase_machine_builder.rb
|
|
116
124
|
- lib/phronomy/agent/react_agent.rb
|
|
117
125
|
- lib/phronomy/agent/runner.rb
|
|
118
126
|
- lib/phronomy/agent/shared_state.rb
|
|
119
127
|
- lib/phronomy/agent/suspend_signal.rb
|
|
120
|
-
- lib/phronomy/agent/
|
|
121
|
-
- lib/phronomy/async_queue.rb
|
|
122
|
-
- lib/phronomy/blocking_adapter_pool.rb
|
|
123
|
-
- lib/phronomy/cancellation_scope.rb
|
|
124
|
-
- lib/phronomy/cancellation_token.rb
|
|
125
|
-
- lib/phronomy/concurrency_gate.rb
|
|
128
|
+
- lib/phronomy/agent/tool_executor.rb
|
|
129
|
+
- lib/phronomy/concurrency/async_queue.rb
|
|
130
|
+
- lib/phronomy/concurrency/blocking_adapter_pool.rb
|
|
131
|
+
- lib/phronomy/concurrency/cancellation_scope.rb
|
|
132
|
+
- lib/phronomy/concurrency/cancellation_token.rb
|
|
133
|
+
- lib/phronomy/concurrency/concurrency_gate.rb
|
|
134
|
+
- lib/phronomy/concurrency/deadline.rb
|
|
135
|
+
- lib/phronomy/concurrency/gate_registry.rb
|
|
136
|
+
- lib/phronomy/concurrency/pool_registry.rb
|
|
126
137
|
- lib/phronomy/configuration.rb
|
|
127
138
|
- lib/phronomy/context.rb
|
|
128
|
-
- lib/phronomy/context/assembler.rb
|
|
129
|
-
- lib/phronomy/context/compaction_context.rb
|
|
130
|
-
- lib/phronomy/context/context_version_cache.rb
|
|
131
|
-
- lib/phronomy/context/token_budget.rb
|
|
132
|
-
- lib/phronomy/context/token_estimator.rb
|
|
133
|
-
- lib/phronomy/context/trigger_context.rb
|
|
134
|
-
- lib/phronomy/context/trim_context.rb
|
|
135
|
-
- lib/phronomy/deadline.rb
|
|
136
139
|
- lib/phronomy/diagnostics.rb
|
|
137
|
-
- lib/phronomy/embeddings.rb
|
|
138
|
-
- lib/phronomy/embeddings/base.rb
|
|
139
|
-
- lib/phronomy/embeddings/ruby_llm_embeddings.rb
|
|
140
140
|
- lib/phronomy/eval.rb
|
|
141
141
|
- lib/phronomy/eval/comparison.rb
|
|
142
142
|
- lib/phronomy/eval/dataset.rb
|
|
@@ -151,7 +151,6 @@ files:
|
|
|
151
151
|
- lib/phronomy/eval/scorer/llm_judge.rb
|
|
152
152
|
- lib/phronomy/event.rb
|
|
153
153
|
- lib/phronomy/event_loop.rb
|
|
154
|
-
- lib/phronomy/fsm_session.rb
|
|
155
154
|
- lib/phronomy/generator_verifier.rb
|
|
156
155
|
- lib/phronomy/guardrail.rb
|
|
157
156
|
- lib/phronomy/guardrail/base.rb
|
|
@@ -160,31 +159,27 @@ files:
|
|
|
160
159
|
- lib/phronomy/guardrail/prompt_injection_guardrail.rb
|
|
161
160
|
- lib/phronomy/invocation_context.rb
|
|
162
161
|
- lib/phronomy/knowledge_source.rb
|
|
163
|
-
- lib/phronomy/knowledge_source/base.rb
|
|
164
|
-
- lib/phronomy/knowledge_source/entity_knowledge.rb
|
|
165
|
-
- lib/phronomy/knowledge_source/rag_knowledge.rb
|
|
166
|
-
- lib/phronomy/knowledge_source/static_knowledge.rb
|
|
167
162
|
- lib/phronomy/llm_adapter.rb
|
|
168
163
|
- lib/phronomy/llm_adapter/base.rb
|
|
169
164
|
- lib/phronomy/llm_adapter/ruby_llm.rb
|
|
170
|
-
- lib/phronomy/
|
|
171
|
-
- lib/phronomy/
|
|
172
|
-
- lib/phronomy/
|
|
173
|
-
- lib/phronomy/
|
|
174
|
-
- lib/phronomy/loader/plain_text_loader.rb
|
|
165
|
+
- lib/phronomy/llm_context_window/assembler.rb
|
|
166
|
+
- lib/phronomy/llm_context_window/context_version_cache.rb
|
|
167
|
+
- lib/phronomy/llm_context_window/token_budget.rb
|
|
168
|
+
- lib/phronomy/llm_context_window/token_estimator.rb
|
|
175
169
|
- lib/phronomy/metrics.rb
|
|
170
|
+
- lib/phronomy/multi_agent/handoff.rb
|
|
171
|
+
- lib/phronomy/multi_agent/orchestrator.rb
|
|
172
|
+
- lib/phronomy/multi_agent/parallel_tool_chat.rb
|
|
173
|
+
- lib/phronomy/multi_agent/team_coordinator.rb
|
|
176
174
|
- lib/phronomy/output_parser.rb
|
|
177
175
|
- lib/phronomy/output_parser/base.rb
|
|
178
176
|
- lib/phronomy/output_parser/json_parser.rb
|
|
179
177
|
- lib/phronomy/output_parser/structured_parser.rb
|
|
180
|
-
- lib/phronomy/prompt_template.rb
|
|
181
178
|
- lib/phronomy/ruby_llm_patches.rb
|
|
182
179
|
- lib/phronomy/runnable.rb
|
|
183
180
|
- lib/phronomy/runtime.rb
|
|
184
181
|
- lib/phronomy/runtime/deterministic_scheduler.rb
|
|
185
182
|
- lib/phronomy/runtime/fake_scheduler.rb
|
|
186
|
-
- lib/phronomy/runtime/gate_registry.rb
|
|
187
|
-
- lib/phronomy/runtime/pool_registry.rb
|
|
188
183
|
- lib/phronomy/runtime/runtime_metrics.rb
|
|
189
184
|
- lib/phronomy/runtime/scheduler.rb
|
|
190
185
|
- lib/phronomy/runtime/scheduler_timer_adapter.rb
|
|
@@ -192,10 +187,6 @@ files:
|
|
|
192
187
|
- lib/phronomy/runtime/thread_scheduler.rb
|
|
193
188
|
- lib/phronomy/runtime/timer_queue.rb
|
|
194
189
|
- lib/phronomy/runtime/timer_service.rb
|
|
195
|
-
- lib/phronomy/splitter.rb
|
|
196
|
-
- lib/phronomy/splitter/base.rb
|
|
197
|
-
- lib/phronomy/splitter/fixed_size_splitter.rb
|
|
198
|
-
- lib/phronomy/splitter/recursive_splitter.rb
|
|
199
190
|
- lib/phronomy/state_store/base.rb
|
|
200
191
|
- lib/phronomy/state_store/in_memory.rb
|
|
201
192
|
- lib/phronomy/task.rb
|
|
@@ -210,11 +201,9 @@ files:
|
|
|
210
201
|
- lib/phronomy/testing/scheduler_helpers.rb
|
|
211
202
|
- lib/phronomy/token_usage.rb
|
|
212
203
|
- lib/phronomy/tool.rb
|
|
213
|
-
- lib/phronomy/
|
|
214
|
-
- lib/phronomy/
|
|
215
|
-
- lib/phronomy/
|
|
216
|
-
- lib/phronomy/tool/scope_policy.rb
|
|
217
|
-
- lib/phronomy/tool_executor.rb
|
|
204
|
+
- lib/phronomy/tools/agent.rb
|
|
205
|
+
- lib/phronomy/tools/mcp.rb
|
|
206
|
+
- lib/phronomy/tools/vector_search.rb
|
|
218
207
|
- lib/phronomy/tracing.rb
|
|
219
208
|
- lib/phronomy/tracing/base.rb
|
|
220
209
|
- lib/phronomy/tracing/langfuse_tracer.rb
|
|
@@ -223,9 +212,18 @@ files:
|
|
|
223
212
|
- lib/phronomy/vector_store.rb
|
|
224
213
|
- lib/phronomy/vector_store/async_backend.rb
|
|
225
214
|
- lib/phronomy/vector_store/base.rb
|
|
215
|
+
- lib/phronomy/vector_store/embeddings/base.rb
|
|
216
|
+
- lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb
|
|
226
217
|
- lib/phronomy/vector_store/in_memory.rb
|
|
218
|
+
- lib/phronomy/vector_store/loader/base.rb
|
|
219
|
+
- lib/phronomy/vector_store/loader/csv_loader.rb
|
|
220
|
+
- lib/phronomy/vector_store/loader/markdown_loader.rb
|
|
221
|
+
- lib/phronomy/vector_store/loader/plain_text_loader.rb
|
|
227
222
|
- lib/phronomy/vector_store/pgvector.rb
|
|
228
223
|
- lib/phronomy/vector_store/redis_search.rb
|
|
224
|
+
- lib/phronomy/vector_store/splitter/base.rb
|
|
225
|
+
- lib/phronomy/vector_store/splitter/fixed_size_splitter.rb
|
|
226
|
+
- lib/phronomy/vector_store/splitter/recursive_splitter.rb
|
|
229
227
|
- lib/phronomy/version.rb
|
|
230
228
|
- lib/phronomy/workflow.rb
|
|
231
229
|
- lib/phronomy/workflow_context.rb
|