phronomy 0.16.0 → 0.17.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +54 -0
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +124 -92
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +1 -1
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  12. data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
  13. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  14. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  15. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
  17. data/lib/phronomy/agent/agent_root.rb +1 -2
  18. data/lib/phronomy/agent/base.rb +135 -314
  19. data/lib/phronomy/agent/context/capability/base.rb +166 -297
  20. data/lib/phronomy/agent/context_assembler.rb +65 -29
  21. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  22. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  23. data/lib/phronomy/agent/execution_coordinator.rb +0 -1
  24. data/lib/phronomy/agent/journal_projection.rb +28 -2
  25. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  26. data/lib/phronomy/agent/shared_state.rb +46 -138
  27. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  28. data/lib/phronomy/agent/tool_invocation.rb +108 -314
  29. data/lib/phronomy/agent.rb +6 -10
  30. data/lib/phronomy/configuration.rb +15 -158
  31. data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
  32. data/lib/phronomy/engine/runtime.rb +15 -230
  33. data/lib/phronomy/engine/task_group.rb +30 -102
  34. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  35. data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
  36. data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
  37. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  38. data/lib/phronomy/version.rb +1 -1
  39. data/lib/phronomy.rb +3 -120
  40. data/scripts/api_snapshot.rb +1 -12
  41. metadata +3 -9
  42. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  43. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  44. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  45. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  46. data/lib/phronomy/knowledge_source.rb +0 -12
  47. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  48. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
@@ -4,95 +4,34 @@ require "securerandom"
4
4
 
5
5
  module Phronomy
6
6
  module MultiAgent
7
- # Implements the "Agent teams" coordination pattern (Anthropic blog, Pattern 3).
8
- #
9
- # @see https://claude.com/blog/multi-agent-coordination-patterns
10
- #
11
- # A coordinator LLM agent decomposes work into tasks and enqueues them
12
- # dynamically via built-in tools. A fixed set of worker agents processes tasks
13
- # sequentially — one task per worker per turn — carrying forward their
14
- # conversation history across assignments to accumulate domain context over time.
15
- #
16
- # Workers are selected in sequence (the worker with the fewest accumulated
17
- # messages is chosen by default). Task dispatch is synchronous; there is no
18
- # concurrent or parallel execution.
19
- #
20
- # The coordinator is an {Agent::Base} subclass that has two built-in tools:
21
- # - +enqueue_task+ — adds a task description to the queue
22
- # - +finalize+ — signals that all tasks have been enqueued
23
- #
24
- # Worker persistence is implemented by passing each worker's accumulated
25
- # +messages+ array back as a top-level +messages:+ argument on every subsequent
26
- # +invoke+ call, so the LLM retains context across multiple task assignments.
27
- #
28
- # @example Basic usage
29
- # class MigrationTeam < Phronomy::MultiAgent::TeamCoordinator
30
- # coordinator_model "claude-3-5-sonnet-20241022"
31
- # coordinator_instructions <<~INST
32
- # Analyze the request and enqueue one migration task per service.
33
- # Call enqueue_task for each service, then call finalize.
34
- # INST
35
- #
36
- # pool size: 3, agent: MigrationAgent
37
- #
38
- # aggregate do |assignments|
39
- # { reports: assignments.map { |a| { task: a[:task][:description], result: a[:result] } } }
40
- # end
41
- # end
42
- #
43
- # result = MigrationTeam.new.invoke("Migrate all services to Rails 8")
7
+ # Coordinator/worker multi-agent pattern with persistent worker Agent state.
44
8
  class TeamCoordinator
45
- # Holds per-worker context between task invocations.
46
- # Worker persistence is implemented by carrying +messages+ forward on each
47
- # successive +agent#invoke+ call as the top-level +messages:+ argument..
48
9
  WorkerState = Struct.new(
49
- :index, # Integer — 0-based worker index
50
- :agent, # Agent::Base instance
51
- :messages, # Array — accumulated conversation history
52
- :status # Symbol — :idle | :available | :done
10
+ :index,
11
+ :agent,
12
+ :transcript_size,
13
+ :status
53
14
  ) do
54
- # Returns true when this worker is ready to accept the next task.
55
- def available? = [:idle, :available].include?(status)
15
+ def available? = %i[idle available].include?(status)
56
16
  end
57
17
  private_constant :WorkerState
58
18
 
59
19
  class << self
60
- # Sets the LLM model for the coordinator agent.
61
- # Falls back to +Phronomy.configuration.default_model+ when not set.
62
- #
63
- # @param value [String, nil]
64
20
  # @api public
65
21
  def coordinator_model(value = nil)
66
22
  value ? @coordinator_model = value : @coordinator_model
67
23
  end
68
24
 
69
- # Sets the system instructions for the coordinator agent.
70
- # The prompt should direct the LLM to call +enqueue_task+ for each task
71
- # and then call +finalize+ when all tasks are enqueued.
72
- #
73
- # @param value [String, nil]
74
25
  # @api public
75
26
  def coordinator_instructions(value = nil)
76
27
  value ? @coordinator_instructions = value : @coordinator_instructions
77
28
  end
78
29
 
79
- # Sets the LLM provider for the coordinator agent.
80
- # Required when using a custom +BASE_URL+ (e.g. LM Studio, Ollama, vLLM)
81
- # so that RubyLLM does not attempt to resolve an unknown model name.
82
- # Pass the same value as +LLMConfig::PROVIDER+ in your examples.
83
- #
84
- # @param value [Symbol, nil]
85
30
  # @api public
86
31
  def coordinator_provider(value = nil)
87
32
  value ? @coordinator_provider = value : @coordinator_provider
88
33
  end
89
34
 
90
- # Configures the set of workers.
91
- #
92
- # @param size [Integer] number of persistent worker instances (tasks are assigned sequentially)
93
- # @param agent [Class] Agent::Base subclass used for all workers
94
- # @param on_error [Symbol] +:raise+ (default) propagates worker exceptions;
95
- # +:skip+ records the failure and continues with remaining tasks
96
35
  # @api public
97
36
  def pool(size:, agent:, on_error: :raise)
98
37
  @pool_size = Integer(size)
@@ -100,57 +39,31 @@ module Phronomy
100
39
  @on_error = on_error
101
40
  end
102
41
 
103
- # Customises the worker selection algorithm.
104
- # The block receives an Array of available WorkerState objects and must
105
- # return the one to assign the next task to.
106
- # Default: worker with the fewest accumulated messages (round-robin-like).
107
- #
108
- # @yield [Array<WorkerState>] available workers
109
- # @yieldreturn [WorkerState] the chosen worker
110
42
  # @api public
111
43
  def schedule(&block)
112
44
  @scheduler = block
113
45
  end
114
46
 
115
- # Defines how task assignments are merged into the final return value.
116
- # The block receives an Array of assignment Hashes:
117
- # { task: Hash, result: String|nil, worker: Integer, error: Exception|nil }
118
- # When omitted, the raw assignments array is returned.
119
- #
120
- # @yield [Array<Hash>] all completed (and skipped) task assignments
121
47
  # @api public
122
48
  def aggregate(&block)
123
49
  @aggregator = block
124
50
  end
125
51
 
126
- # @!visibility private
127
52
  def _coordinator_model = @coordinator_model
128
- # @!visibility private
129
53
  def _coordinator_instructions = @coordinator_instructions
130
- # @!visibility private
131
54
  def _coordinator_provider = @coordinator_provider
132
- # @!visibility private
133
55
  def _pool_size = @pool_size || 1
134
- # @!visibility private
135
56
  def _worker_agent = @worker_agent
136
- # @!visibility private
137
57
  def _on_error = @on_error || :raise
138
- # @!visibility private
139
58
  def _scheduler = @scheduler
140
- # @!visibility private
141
59
  def _aggregator = @aggregator
142
60
  end
143
61
 
144
- # Runs the full team coordination: coordinator generates tasks, workers
145
- # process them sequentially, and the aggregate block merges the results.
146
- #
147
- # @param team_input [String, Hash] the high-level objective given to the coordinator
148
- # @param config [Hash] reserved for future use
149
- # @return [Object] the return value of the aggregate block, or the raw assignments Array
150
- # @raise [ArgumentError] when +pool :agent+ has not been configured
151
62
  # @api public
152
63
  def invoke(team_input, config: {})
153
- raise ArgumentError, "pool :agent must be configured before invoking" unless self.class._worker_agent
64
+ unless self.class._worker_agent
65
+ raise ArgumentError, "pool :agent must be configured before invoking"
66
+ end
154
67
 
155
68
  task_queue = []
156
69
  run_coordinator(team_input, task_queue)
@@ -158,26 +71,13 @@ module Phronomy
158
71
  finalize_result(assignments)
159
72
  end
160
73
 
161
- # Streaming version of +invoke+. Yields a Hash event for each completed or
162
- # failed task assignment.
163
- #
164
- # Yielded Hash keys:
165
- # :type — +:task_completed+ or +:task_failed+
166
- # :worker — worker index (Integer)
167
- # :task — the task Hash from the queue ({ id:, description:, metadata:, enqueued_at: })
168
- # :result — output string, or +nil+ on failure
169
- # :error — Exception, or +nil+ on success
170
- #
171
- # @param team_input [String, Hash]
172
- # @param config [Hash]
173
- # @yield [Hash] one event per completed/failed task
174
- # @return [Object] same as +invoke+
175
- # @raise [ArgumentError] when +pool :agent+ has not been configured
176
74
  # @api public
177
75
  def stream(team_input, config: {}, &block)
178
76
  return invoke(team_input, config: config) unless block
179
77
 
180
- raise ArgumentError, "pool :agent must be configured before invoking" unless self.class._worker_agent
78
+ unless self.class._worker_agent
79
+ raise ArgumentError, "pool :agent must be configured before invoking"
80
+ end
181
81
 
182
82
  task_queue = []
183
83
  run_coordinator(team_input, task_queue)
@@ -187,23 +87,25 @@ module Phronomy
187
87
 
188
88
  private
189
89
 
190
- # Phase 1: Run the coordinator LLM agent to populate task_queue.
191
90
  def run_coordinator(team_input, task_queue)
192
91
  coordinator = build_coordinator_agent(task_queue)
193
92
  input = team_input.is_a?(String) ? team_input : team_input.to_s
194
93
  coordinator.invoke(input)
195
94
  end
196
95
 
197
- # Phase 2: Process tasks from the queue using the worker pool.
198
- # Workers accumulate message history across assignments.
199
96
  def run_workers(task_queue, &event_block)
200
97
  pool_size = self.class._pool_size
201
98
  agent_class = self.class._worker_agent
202
99
  on_error = self.class._on_error
203
100
  scheduler = self.class._scheduler
204
101
 
205
- workers = Array.new(pool_size) do |i|
206
- WorkerState.new(index: i, agent: agent_class.new, messages: [], status: :idle)
102
+ workers = Array.new(pool_size) do |index|
103
+ WorkerState.new(
104
+ index: index,
105
+ agent: agent_class.new,
106
+ transcript_size: 0,
107
+ status: :idle
108
+ )
207
109
  end
208
110
 
209
111
  assignments = []
@@ -214,40 +116,45 @@ module Phronomy
214
116
  worker = scheduler ? scheduler.call(available) : default_scheduler(available)
215
117
 
216
118
  begin
217
- # Worker agent retains conversation history in its Journal.
218
119
  result = worker.agent.invoke(task[:description])
219
- worker.messages = Array(result[:messages])
120
+ worker.transcript_size = worker.agent.transcript.length
220
121
  worker.status = :available
221
- entry = {task: task, result: result[:output], worker: worker.index, error: nil}
122
+ entry = {
123
+ task: task,
124
+ result: result[:output],
125
+ worker: worker.index,
126
+ error: nil
127
+ }
222
128
  assignments << entry
223
129
  event_block&.call(entry.merge(type: :task_completed))
224
- rescue => e
130
+ rescue => error
225
131
  worker.status = :available
226
132
  raise unless on_error == :skip
227
133
 
228
- entry = {task: task, result: nil, worker: worker.index, error: e}
134
+ entry = {
135
+ task: task,
136
+ result: nil,
137
+ worker: worker.index,
138
+ error: error
139
+ }
229
140
  assignments << entry
230
141
  event_block&.call(entry.merge(type: :task_failed))
231
142
  end
232
143
  end
233
144
 
234
- workers.each { |w| w.status = :done }
145
+ workers.each { |worker| worker.status = :done }
235
146
  assignments
236
147
  end
237
148
 
238
- # Phase 3: Apply the aggregate block (or return raw assignments).
239
149
  def finalize_result(assignments)
240
150
  aggregator = self.class._aggregator
241
151
  aggregator ? aggregator.call(assignments) : assignments
242
152
  end
243
153
 
244
- # Default scheduler: assign to the worker with the fewest accumulated
245
- # messages (promotes round-robin-like distribution across the pool).
246
154
  def default_scheduler(available_workers)
247
- available_workers.min_by { |w| w.messages.size }
155
+ available_workers.min_by(&:transcript_size)
248
156
  end
249
157
 
250
- # Build an anonymous coordinator Agent::Base with the two built-in tools.
251
158
  def build_coordinator_agent(task_queue)
252
159
  coordinator_model_val = self.class._coordinator_model
253
160
  coordinator_instructions_val = self.class._coordinator_instructions
@@ -260,13 +167,12 @@ module Phronomy
260
167
  model coordinator_model_val
261
168
  provider coordinator_provider_val if coordinator_provider_val
262
169
  instructions coordinator_instructions_val
263
- tools enqueue_tool, finalize_tool
170
+ tools(enqueue_tool => nil, finalize_tool => nil)
264
171
  end
265
172
 
266
173
  coordinator_class.new
267
174
  end
268
175
 
269
- # Builds the +enqueue_task+ tool. Each call appends a task Hash to task_queue.
270
176
  def build_enqueue_tool(task_queue)
271
177
  Class.new(Phronomy::Agent::Context::Capability::Base) do
272
178
  tool_name "enqueue_task"
@@ -275,15 +181,18 @@ module Phronomy
275
181
  param :metadata, type: :string, desc: "Optional metadata", required: false
276
182
 
277
183
  define_method(:execute) do |description:, metadata: nil|
278
- task = {id: task_queue.size + 1, description: description, metadata: metadata, enqueued_at: Time.now}
184
+ task = {
185
+ id: task_queue.size + 1,
186
+ description: description,
187
+ metadata: metadata,
188
+ enqueued_at: Time.now
189
+ }
279
190
  task_queue << task
280
191
  "Task ##{task[:id]} enqueued: #{description}"
281
192
  end
282
193
  end
283
194
  end
284
195
 
285
- # Builds the +finalize+ tool. Signals to the coordinator LLM that all tasks
286
- # have been enqueued; returns a confirmation string.
287
196
  def build_finalize_tool(task_queue)
288
197
  Class.new(Phronomy::Agent::Context::Capability::Base) do
289
198
  tool_name "finalize"
@@ -4,8 +4,8 @@ module Phronomy
4
4
  module VectorStore
5
5
  # Pure-Ruby in-memory vector store using cosine similarity.
6
6
  #
7
- # Intended for tests, short-lived agents, and Retrieval::Semantic scenarios where
8
- # the message count is small enough that a linear scan is fast enough.
7
+ # Intended for tests, short-lived agents, and small retrieval workloads where
8
+ # the document count is small enough that a linear scan is fast enough.
9
9
  #
10
10
  # @example
11
11
  # store = Phronomy::VectorStore::InMemory.new
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/phronomy.rb CHANGED
@@ -5,36 +5,18 @@ require "ruby_llm"
5
5
  require_relative "phronomy/ruby_llm_patches"
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
- # Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
9
- # ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
10
8
  loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
11
- # RAG: Zeitwerk would infer "Rag" — override to "RAG".
12
9
  loader.inflector.inflect("rag" => "RAG")
13
- # FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
14
- # Phronomy::FSMSession is the top-level cooperative execution engine shared by
15
- # WorkflowRunner, AgentInvocationSessionBuilder, and ToolInvocationSessionBuilder.
16
10
  loader.inflector.inflect("fsm_session" => "FSMSession")
17
- # LLMAdapter: Zeitwerk would infer "LlmAdapter" — override to "LLMAdapter".
18
11
  loader.inflector.inflect("llm_adapter" => "LLMAdapter")
19
- # LLMOperationResult: preserve the LLM acronym for the Agent result carrier.
20
12
  loader.inflector.inflect("llm_operation_result" => "LLMOperationResult")
21
- # LLMAdapter::RubyLLM: "ruby_llm" maps to "RubyLLM" (not "RubyLlm").
22
13
  loader.inflector.inflect("ruby_llm" => "RubyLLM")
23
- # CanonicalJSON: preserve uppercase JSON acronym.
24
14
  loader.inflector.inflect("canonical_json" => "CanonicalJSON")
25
- # RubyLLMMaterializer: preserve the double-uppercase LLM acronym.
26
15
  loader.inflector.inflect("ruby_llm_materializer" => "RubyLLMMaterializer")
27
- # LLMCallRecord: preserve uppercase LLM acronym.
28
16
  loader.inflector.inflect("llm_call_record" => "LLMCallRecord")
29
- # LLMInputManifest: preserve uppercase LLM acronym.
30
17
  loader.inflector.inflect("llm_input_manifest" => "LLMInputManifest")
31
- # LLMInputBuildContext / LLMInputPatch: preserve uppercase LLM acronym.
32
18
  loader.inflector.inflect("llm_input_build_context" => "LLMInputBuildContext")
33
19
  loader.inflector.inflect("llm_input_patch" => "LLMInputPatch")
34
- # Collapse engine/ so that its contents autoload directly under Phronomy::
35
- # (no Engine:: prefix). e.g. engine/event_loop.rb => Phronomy::EventLoop.
36
- # This allows the execution engine to be organised in its own subdirectory
37
- # without changing any class names or callers.
38
20
  loader.collapse("#{__dir__}/phronomy/engine")
39
21
  loader.setup
40
22
 
@@ -42,70 +24,25 @@ require_relative "phronomy/version"
42
24
  require_relative "phronomy/token_usage"
43
25
 
44
26
  module Phronomy
45
- # Exception hierarchy
46
27
  class Error < StandardError; end
47
28
  class ParseError < Error; end
48
29
  class RecursionLimitError < Error; end
49
30
  class ToolError < Error; end
50
- # Base error for Phronomy-owned timed boundaries and generic timeout primitives.
51
31
  class TimeoutError < Error; end
52
-
53
32
  class ConfigurationError < Error; end
54
-
55
33
  class HandoffError < Error; end
56
34
 
57
- # Raised when a network or transport layer call fails (e.g. LLM API unreachable,
58
- # MCP server connection refused). Distinguishable from application-level errors
59
- # so callers can apply network-specific retry logic.
60
35
  class TransportError < Error; end
61
-
62
- # Raised when the LLM API returns a rate-limit response (HTTP 429 or equivalent).
63
- # Callers should back off and retry after the indicated delay.
64
36
  class RateLimitError < TransportError; end
65
-
66
- # Raised when the LLM API rejects the request due to an invalid or revoked API key.
67
- # Callers should not retry without fixing the credentials.
68
37
  class AuthenticationError < TransportError; end
69
-
70
- # Raised when the prompt exceeds the model's context window limit.
71
38
  class ContextLengthError < Error; end
72
-
73
- # Raised when a workflow or agent execution is explicitly cancelled.
74
- # Separate from TimeoutError (deadline exceeded) — this is an intentional stop.
75
39
  class CancellationError < Error; end
76
40
 
77
- # Raised when {Agent#invoke} (a synchronous, blocking call) is attempted from
78
- # inside an active scheduler task and +strict_runtime_guards+ is enabled.
79
- #
80
- # Calling a blocking invocation from within a scheduler task stalls the
81
- # scheduler until the inner invocation completes, preventing other tasks from
82
- # making progress (hidden deadlock risk). Use {Agent#invoke_async} followed by
83
- # +#await+ inside scheduler tasks instead.
84
- #
85
- # This error is only raised when:
86
- # Phronomy.configure { |c| c.strict_runtime_guards = true }
87
- #
88
- # By default a warning is logged and execution continues.
89
- #
90
- # @see Phronomy::Runtime.in_scheduler_context?
91
41
  class SchedulerReentrancyError < Error; end
92
-
93
- # Raised when work is submitted to a Runtime whose shutdown has begun, or
94
- # when a Runtime cannot be reset because owned resources are still alive.
95
42
  class RuntimeShutdownError < Error; end
96
-
97
- # Raised when Runtime#shutdown is invoked from inside a Phronomy::Task.
98
43
  class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
99
44
 
100
- # Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
101
- # and the pipeline's combined confidence score falls below the configured threshold.
102
- #
103
- # @example
104
- # rescue Phronomy::LowConfidenceError => e
105
- # puts e.result.confidence # => e.g. 0.45
106
- # puts e.result.output # best-effort answer despite low confidence
107
45
  class LowConfidenceError < Error
108
- # @return [Phronomy::GeneratorVerifier::Result] the untrusted result
109
46
  attr_reader :result
110
47
 
111
48
  def initialize(result)
@@ -114,9 +51,6 @@ module Phronomy
114
51
  end
115
52
  end
116
53
 
117
- # Raised by a {Phronomy::Filter::Base} subclass when the filter rejects a
118
- # value without transforming it (blocking the pipeline).
119
- # @api public
120
54
  class FilterBlockError < Error
121
55
  attr_reader :filter
122
56
 
@@ -126,27 +60,12 @@ module Phronomy
126
60
  end
127
61
  end
128
62
 
129
- # Raised when an operation is submitted to a {BlockingAdapterPool} that has
130
- # already been shut down via {BlockingAdapterPool#shutdown}.
131
63
  class PoolShutdownError < Error; end
132
-
133
- # Raised when a concurrency limit is exceeded and the configured backpressure
134
- # strategy is +:raise+. The caller should back off and retry.
135
64
  class BackpressureError < Error; end
136
65
 
137
- # Raised by {CancellationScope#pop_queue} when the deadline expires before a
138
- # result is available. Extends {TimeoutError} for backwards compatibility.
139
- class ScopeTimeoutError < TimeoutError; end
140
-
141
- # Deprecated compatibility constant. Workflow entry/exit actions are
142
- # synchronous and the Workflow DSL no longer accepts +action_timeout:+.
143
- class ActionTimeoutError < TimeoutError; end
144
-
145
- # Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
146
- # that does not own the context (i.e. not the EventLoop dispatch thread).
147
- # Only raised in EventLoop mode. Use +context.merge(...)+ to produce a new
148
- # context, or deliver updates as +:action_completed+ event payloads
149
- # via {Agent::Base#invoke_async} + {Task#map}.
66
+ # Raised when a WorkflowContext field is mutated from a thread that does not
67
+ # own the context. Deliver asynchronous updates back to the Workflow as later
68
+ # events via Workflow#signal instead of mutating context from worker callbacks.
150
69
  class WorkflowContextOwnershipError < Error; end
151
70
 
152
71
  class << self
@@ -158,36 +77,10 @@ module Phronomy
158
77
  yield configuration
159
78
  end
160
79
 
161
- # Resets the global Phronomy configuration to defaults.
162
- #
163
- # **Intended for test suites only.** Calling this in a production process
164
- # will drop all runtime configuration (tracer, model, tokenizer, etc.)
165
- # globally and immediately affect all subsequent agent and workflow calls.
166
- #
167
- # **Parallel test suites warning:** When tests run in parallel (e.g.
168
- # `parallel_tests` or `parallel_rspec`), +reset_configuration!+ in one
169
- # worker will clear configuration shared with other workers in the same
170
- # process. Prefer process-isolation strategies (forked workers) over
171
- # thread-based parallelism when using this method.
172
- #
173
- # Typical usage in a sequential test suite:
174
- # after { Phronomy.reset_configuration! }
175
80
  def reset_configuration!
176
81
  @configuration = Configuration.new
177
82
  end
178
83
 
179
- # Yields the current {Configuration} object, then restores the original
180
- # configuration on exit (even if the block raises).
181
- #
182
- # Intended for test helpers that need to temporarily override settings
183
- # without permanently mutating the global configuration.
184
- #
185
- # @yield [config] the current {Configuration} instance (mutable)
186
- # @example
187
- # Phronomy.with_configuration do |c|
188
- # c.logger = Logger.new($stdout)
189
- # end
190
- # @api public
191
84
  def with_configuration
192
85
  original = @configuration&.dup
193
86
  yield configuration
@@ -195,16 +88,6 @@ module Phronomy
195
88
  @configuration = original
196
89
  end
197
90
 
198
- # Shuts down and clears the process-wide default Runtime, then resets
199
- # global configuration. Intended for test suites only.
200
- #
201
- # Runtime execution failure and resource cleanup are separate. The
202
- # singleton is cleared when cleanup completed, even if execution failed.
203
- #
204
- # @param timeout [Numeric] maximum graceful wait for Runtime tasks and
205
- # EventLoop shutdown
206
- # @return [Phronomy::Runtime::ShutdownResult]
207
- # @api public
208
91
  def reset_runtime!(timeout: configuration.event_loop_stop_grace_seconds)
209
92
  previous_grace = @configuration&.event_loop_stop_grace_seconds
210
93
  result = Runtime.reset_default!(timeout: timeout)
@@ -4,23 +4,17 @@
4
4
  # scripts/api_snapshot.rb
5
5
  #
6
6
  # Dumps the public instance methods of all Stable/Beta public API classes to
7
- # JSON. The snapshot is stored in spec/fixtures/api_snapshot.json and is used
7
+ # JSON. The snapshot is stored in spec/fixtures/api_snapshot.json and is used
8
8
  # by spec/phronomy/api_compatibility_spec.rb to detect unintended API removals.
9
9
  #
10
10
  # Usage:
11
- # # Regenerate spec/fixtures/api_snapshot.json (run when intentionally adding
12
- # # or removing public API methods after updating the stability table):
13
11
  # ruby scripts/api_snapshot.rb --write
14
- #
15
- # # Print snapshot to stdout (useful for manual inspection):
16
12
  # ruby scripts/api_snapshot.rb
17
13
 
18
14
  require "json"
19
15
  require "fileutils"
20
16
  require_relative "../lib/phronomy"
21
17
 
22
- # Classes and modules whose public API is tracked.
23
- # Add an entry whenever a new class/module is promoted to Stable or Beta in README.md.
24
18
  PUBLIC_API_ENTRIES = [
25
19
  # Stable
26
20
  Phronomy::Agent::Base,
@@ -37,8 +31,6 @@ PUBLIC_API_ENTRIES = [
37
31
  Phronomy::VectorStore::Base,
38
32
  Phronomy::VectorStore::InMemory,
39
33
  Phronomy::VectorStore::Embeddings::Base,
40
- Phronomy::Agent::Context::Knowledge::Base,
41
- Phronomy::Agent::Context::Knowledge::StaticKnowledge,
42
34
  Phronomy::Tracing::Base,
43
35
  Phronomy::Tracing::NullTracer,
44
36
  Phronomy::Eval::Runner,
@@ -47,7 +39,6 @@ PUBLIC_API_ENTRIES = [
47
39
  Phronomy::Tools::VectorSearch
48
40
  ].freeze
49
41
 
50
- # Baseline methods common to all Ruby objects — excluded from the snapshot.
51
42
  BASELINE_INSTANCE_METHODS = (
52
43
  Object.public_instance_methods |
53
44
  Kernel.public_instance_methods
@@ -60,7 +51,6 @@ BASELINE_CLASS_METHODS = (
60
51
 
61
52
  def snapshot_entry(klass)
62
53
  if klass.instance_of?(Module)
63
- # Module — capture instance methods defined in this module only
64
54
  own_methods = klass.public_instance_methods(false).sort
65
55
  {
66
56
  "name" => klass.name,
@@ -68,7 +58,6 @@ def snapshot_entry(klass)
68
58
  "public_instance_methods" => own_methods
69
59
  }
70
60
  else
71
- # Class — capture public instance methods minus universal baseline
72
61
  instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS).sort
73
62
  class_methods = (klass.public_methods(false) - BASELINE_CLASS_METHODS).sort
74
63
  {
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.16.0
4
+ version: 0.17.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-08-08 00:00:00.000000000 Z
11
+ date: 2026-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -117,6 +117,7 @@ files:
117
117
  - docs/decisions/011-build-context-as-single-llm-input-authority.md
118
118
  - docs/decisions/011-delegate-transport-policy-to-adapters.md
119
119
  - docs/decisions/012-canonical-execution-log-and-context-policy.md
120
+ - docs/decisions/013-journal-backed-knowledge-as-context-candidates.md
120
121
  - docs/mcp-client.md
121
122
  - examples/workflows/agent_event_mapping.rb
122
123
  - examples/workflows/generic_task_event_mapping.rb
@@ -137,9 +138,6 @@ files:
137
138
  - lib/phronomy/agent/concerns/filterable.rb
138
139
  - lib/phronomy/agent/context/capability/base.rb
139
140
  - lib/phronomy/agent/context/instruction/prompt_template.rb
140
- - lib/phronomy/agent/context/knowledge/base.rb
141
- - lib/phronomy/agent/context/knowledge/entity_knowledge.rb
142
- - lib/phronomy/agent/context/knowledge/static_knowledge.rb
143
141
  - lib/phronomy/agent/context_assembler.rb
144
142
  - lib/phronomy/agent/context_candidate.rb
145
143
  - lib/phronomy/agent/context_candidate_resolver.rb
@@ -159,7 +157,6 @@ files:
159
157
  - lib/phronomy/agent/context_selection_unit.rb
160
158
  - lib/phronomy/agent/derived_content_spec.rb
161
159
  - lib/phronomy/agent/execution_coordinator.rb
162
- - lib/phronomy/agent/fsm_runtime_adapter.rb
163
160
  - lib/phronomy/agent/immutable.rb
164
161
  - lib/phronomy/agent/journal_projection.rb
165
162
  - lib/phronomy/agent/journal_record.rb
@@ -236,12 +233,9 @@ files:
236
233
  - lib/phronomy/invalid_async_workflow_action_error.rb
237
234
  - lib/phronomy/invalid_context_budget_configuration_error.rb
238
235
  - lib/phronomy/invocation_context.rb
239
- - lib/phronomy/knowledge_source.rb
240
236
  - lib/phronomy/llm_adapter.rb
241
237
  - lib/phronomy/llm_adapter/base.rb
242
238
  - lib/phronomy/llm_adapter/ruby_llm.rb
243
- - lib/phronomy/llm_context_window/assembler.rb
244
- - lib/phronomy/llm_context_window/context_version_cache.rb
245
239
  - lib/phronomy/llm_context_window/token_budget.rb
246
240
  - lib/phronomy/llm_context_window/token_estimator.rb
247
241
  - lib/phronomy/metrics.rb