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.
- checksums.yaml +4 -4
- data/.mutant.yml +8 -9
- data/CHANGELOG.md +54 -0
- data/CONTRIBUTING.md +28 -16
- data/README.md +124 -92
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +1 -1
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +156 -93
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/base.rb +135 -314
- data/lib/phronomy/agent/context/capability/base.rb +166 -297
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +0 -1
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +46 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_invocation.rb +108 -314
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +15 -158
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
- data/lib/phronomy/engine/runtime.rb +15 -230
- data/lib/phronomy/engine/task_group.rb +30 -102
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/multi_agent/orchestrator.rb +152 -204
- data/lib/phronomy/multi_agent/team_coordinator.rb +42 -133
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +3 -120
- data/scripts/api_snapshot.rb +1 -12
- metadata +3 -9
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- 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
|
-
#
|
|
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,
|
|
50
|
-
:agent,
|
|
51
|
-
:
|
|
52
|
-
:status
|
|
10
|
+
:index,
|
|
11
|
+
:agent,
|
|
12
|
+
:transcript_size,
|
|
13
|
+
:status
|
|
53
14
|
) do
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
206
|
-
WorkerState.new(
|
|
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.
|
|
120
|
+
worker.transcript_size = worker.agent.transcript.length
|
|
220
121
|
worker.status = :available
|
|
221
|
-
entry = {
|
|
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 =>
|
|
130
|
+
rescue => error
|
|
225
131
|
worker.status = :available
|
|
226
132
|
raise unless on_error == :skip
|
|
227
133
|
|
|
228
|
-
entry = {
|
|
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 { |
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
|
8
|
-
# the
|
|
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
|
data/lib/phronomy/version.rb
CHANGED
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
|
|
138
|
-
#
|
|
139
|
-
|
|
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)
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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-
|
|
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
|