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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -45
  3. data/benchmark/baseline.json +1 -1
  4. data/benchmark/bench_agent_invoke.rb +1 -1
  5. data/benchmark/bench_context_assembler.rb +11 -3
  6. data/benchmark/bench_regression.rb +11 -11
  7. data/benchmark/bench_token_estimator.rb +5 -5
  8. data/benchmark/bench_tool_schema.rb +2 -2
  9. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +224 -0
  10. data/lib/phronomy/agent/base.rb +268 -403
  11. data/lib/phronomy/agent/checkpoint.rb +118 -0
  12. data/lib/phronomy/agent/concerns/suspendable.rb +6 -6
  13. data/lib/phronomy/agent/context/capability/base.rb +689 -0
  14. data/lib/phronomy/agent/context/capability/scope_policy.rb +54 -0
  15. data/lib/phronomy/agent/context/instruction/prompt_template.rb +102 -0
  16. data/lib/phronomy/agent/context/knowledge/base.rb +58 -0
  17. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +102 -0
  18. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +58 -0
  19. data/lib/phronomy/agent/fsm.rb +1 -1
  20. data/lib/phronomy/agent/invocation_pipeline.rb +108 -0
  21. data/lib/phronomy/agent/lifecycle/fsm_session.rb +251 -0
  22. data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +249 -0
  23. data/lib/phronomy/agent/react_agent.rb +43 -37
  24. data/lib/phronomy/agent/runner.rb +2 -2
  25. data/lib/phronomy/agent/shared_state.rb +2 -2
  26. data/lib/phronomy/agent/tool_executor.rb +108 -0
  27. data/lib/phronomy/concurrency/async_queue.rb +157 -0
  28. data/lib/phronomy/concurrency/blocking_adapter_pool.rb +443 -0
  29. data/lib/phronomy/concurrency/cancellation_scope.rb +125 -0
  30. data/lib/phronomy/concurrency/cancellation_token.rb +140 -0
  31. data/lib/phronomy/concurrency/concurrency_gate.rb +157 -0
  32. data/lib/phronomy/concurrency/deadline.rb +65 -0
  33. data/lib/phronomy/{runtime → concurrency}/gate_registry.rb +1 -2
  34. data/lib/phronomy/{runtime → concurrency}/pool_registry.rb +1 -1
  35. data/lib/phronomy/configuration.rb +0 -6
  36. data/lib/phronomy/context.rb +2 -8
  37. data/lib/phronomy/eval/runner.rb +4 -0
  38. data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
  39. data/lib/phronomy/event_loop.rb +7 -7
  40. data/lib/phronomy/invocation_context.rb +3 -3
  41. data/lib/phronomy/knowledge_source.rb +0 -5
  42. data/lib/phronomy/llm_adapter/ruby_llm.rb +17 -11
  43. data/lib/phronomy/llm_context_window/assembler.rb +191 -0
  44. data/lib/phronomy/{context → llm_context_window}/context_version_cache.rb +1 -1
  45. data/lib/phronomy/{context → llm_context_window}/token_budget.rb +7 -4
  46. data/lib/phronomy/{context → llm_context_window}/token_estimator.rb +3 -3
  47. data/lib/phronomy/{agent → multi_agent}/handoff.rb +6 -6
  48. data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +7 -7
  49. data/lib/phronomy/{agent → multi_agent}/parallel_tool_chat.rb +4 -4
  50. data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +4 -4
  51. data/lib/phronomy/runtime/runtime_metrics.rb +0 -1
  52. data/lib/phronomy/runtime.rb +20 -6
  53. data/lib/phronomy/task_group.rb +1 -1
  54. data/lib/phronomy/tool.rb +3 -4
  55. data/lib/phronomy/{tool/agent_tool.rb → tools/agent.rb} +6 -6
  56. data/lib/phronomy/{tool/mcp_tool.rb → tools/mcp.rb} +9 -9
  57. data/lib/phronomy/tools/vector_search.rb +70 -0
  58. data/lib/phronomy/tracing/null_tracer.rb +3 -1
  59. data/lib/phronomy/vector_store/async_backend.rb +4 -4
  60. data/lib/phronomy/vector_store/base.rb +2 -2
  61. data/lib/phronomy/vector_store/embeddings/base.rb +41 -0
  62. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +47 -0
  63. data/lib/phronomy/vector_store/in_memory.rb +12 -2
  64. data/lib/phronomy/vector_store/loader/base.rb +27 -0
  65. data/lib/phronomy/vector_store/loader/csv_loader.rb +58 -0
  66. data/lib/phronomy/vector_store/loader/markdown_loader.rb +78 -0
  67. data/lib/phronomy/vector_store/loader/plain_text_loader.rb +24 -0
  68. data/lib/phronomy/vector_store/pgvector.rb +2 -2
  69. data/lib/phronomy/vector_store/redis_search.rb +2 -2
  70. data/lib/phronomy/vector_store/splitter/base.rb +49 -0
  71. data/lib/phronomy/vector_store/splitter/fixed_size_splitter.rb +53 -0
  72. data/lib/phronomy/vector_store/splitter/recursive_splitter.rb +107 -0
  73. data/lib/phronomy/vector_store.rb +14 -2
  74. data/lib/phronomy/version.rb +1 -1
  75. data/lib/phronomy/workflow_context.rb +8 -0
  76. data/lib/phronomy/workflow_runner.rb +11 -131
  77. data/lib/phronomy.rb +2 -0
  78. data/scripts/api_snapshot.rb +11 -9
  79. metadata +44 -46
  80. data/lib/phronomy/async_queue.rb +0 -155
  81. data/lib/phronomy/blocking_adapter_pool.rb +0 -435
  82. data/lib/phronomy/cancellation_scope.rb +0 -123
  83. data/lib/phronomy/cancellation_token.rb +0 -133
  84. data/lib/phronomy/concurrency_gate.rb +0 -155
  85. data/lib/phronomy/context/assembler.rb +0 -143
  86. data/lib/phronomy/context/compaction_context.rb +0 -111
  87. data/lib/phronomy/context/trigger_context.rb +0 -39
  88. data/lib/phronomy/context/trim_context.rb +0 -75
  89. data/lib/phronomy/deadline.rb +0 -63
  90. data/lib/phronomy/embeddings/base.rb +0 -39
  91. data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
  92. data/lib/phronomy/embeddings.rb +0 -11
  93. data/lib/phronomy/fsm_session.rb +0 -247
  94. data/lib/phronomy/knowledge_source/base.rb +0 -54
  95. data/lib/phronomy/knowledge_source/entity_knowledge.rb +0 -96
  96. data/lib/phronomy/knowledge_source/rag_knowledge.rb +0 -57
  97. data/lib/phronomy/knowledge_source/static_knowledge.rb +0 -52
  98. data/lib/phronomy/loader/base.rb +0 -25
  99. data/lib/phronomy/loader/csv_loader.rb +0 -56
  100. data/lib/phronomy/loader/markdown_loader.rb +0 -76
  101. data/lib/phronomy/loader/plain_text_loader.rb +0 -22
  102. data/lib/phronomy/loader.rb +0 -13
  103. data/lib/phronomy/prompt_template.rb +0 -96
  104. data/lib/phronomy/splitter/base.rb +0 -47
  105. data/lib/phronomy/splitter/fixed_size_splitter.rb +0 -51
  106. data/lib/phronomy/splitter/recursive_splitter.rb +0 -105
  107. data/lib/phronomy/splitter.rb +0 -12
  108. data/lib/phronomy/tool/base.rb +0 -644
  109. data/lib/phronomy/tool/scope_policy.rb +0 -50
  110. 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 implementations for embedding-based semantic search.
4
+ # Vector store infrastructure: backends, embeddings adapters, document loaders,
5
+ # and text splitters.
5
6
  #
6
- # Sub-classes are auto-loaded by Zeitwerk:
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.7.1"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -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 = build_phase_machine_class(auto_transitions, exit_actions)
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".
@@ -24,26 +24,28 @@ require_relative "../lib/phronomy"
24
24
  PUBLIC_API_ENTRIES = [
25
25
  # Stable
26
26
  Phronomy::Agent::Base,
27
- Phronomy::Tool::Base,
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::Agent::Orchestrator,
35
- Phronomy::Agent::TeamCoordinator,
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::KnowledgeSource::Base,
42
- Phronomy::KnowledgeSource::StaticKnowledge,
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.7.1
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-05-26 00:00:00.000000000 Z
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 Agent, Workflow, Tool, Guardrail, RAG, and Multi-agent
68
- capabilities for building AI agents in Ruby. Powered by RubyLLM for LLM abstraction.
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/handoff.rb
114
- - lib/phronomy/agent/orchestrator.rb
115
- - lib/phronomy/agent/parallel_tool_chat.rb
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/team_coordinator.rb
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/loader.rb
171
- - lib/phronomy/loader/base.rb
172
- - lib/phronomy/loader/csv_loader.rb
173
- - lib/phronomy/loader/markdown_loader.rb
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/tool/agent_tool.rb
214
- - lib/phronomy/tool/base.rb
215
- - lib/phronomy/tool/mcp_tool.rb
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