phronomy 0.14.0 → 0.15.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +236 -57
  4. data/benchmark/bench_agent_invoke.rb +2 -3
  5. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
  6. data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
  7. data/examples/workflows/agent_event_mapping.rb +104 -0
  8. data/examples/workflows/generic_task_event_mapping.rb +58 -0
  9. data/lib/phronomy/agent/agent_invocation.rb +385 -0
  10. data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
  11. data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
  12. data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
  13. data/lib/phronomy/agent/async_event_api.rb +471 -0
  14. data/lib/phronomy/agent/base.rb +500 -411
  15. data/lib/phronomy/agent/context/capability/base.rb +51 -119
  16. data/lib/phronomy/agent/llm_operation_result.rb +23 -0
  17. data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
  18. data/lib/phronomy/agent/tool_approval_request.rb +121 -0
  19. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
  20. data/lib/phronomy/agent/tool_executor.rb +47 -69
  21. data/lib/phronomy/agent/tool_invocation.rb +634 -0
  22. data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
  23. data/lib/phronomy/agent.rb +21 -9
  24. data/lib/phronomy/configuration.rb +42 -6
  25. data/lib/phronomy/engine/event_loop.rb +269 -112
  26. data/lib/phronomy/engine/fsm_session.rb +180 -142
  27. data/lib/phronomy/engine/task.rb +5 -10
  28. data/lib/phronomy/event.rb +8 -8
  29. data/lib/phronomy/generator_verifier.rb +253 -142
  30. data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
  31. data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
  32. data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
  33. data/lib/phronomy/invocation_context.rb +5 -19
  34. data/lib/phronomy/llm_adapter/base.rb +25 -34
  35. data/lib/phronomy/metrics.rb +2 -0
  36. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
  37. data/lib/phronomy/stream_callback_error.rb +35 -0
  38. data/lib/phronomy/tools/mcp.rb +25 -0
  39. data/lib/phronomy/version.rb +1 -1
  40. data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
  41. data/lib/phronomy/workflow.rb +122 -261
  42. data/lib/phronomy/workflow_context.rb +54 -102
  43. data/lib/phronomy/workflow_runner.rb +238 -300
  44. data/lib/phronomy.rb +6 -4
  45. data/scripts/check_readme_runnable.rb +4 -1
  46. metadata +18 -7
  47. data/lib/phronomy/agent/concerns/retryable.rb +0 -103
  48. data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
  49. data/lib/phronomy/agent/invocation_context.rb +0 -171
  50. data/lib/phronomy/agent/invocation_session.rb +0 -352
  51. data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
@@ -1,118 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  module Phronomy
4
- # Implements the Generator-Verifier multi-agent coordination pattern
5
- # (Anthropic blog, Pattern 1): a generator agent produces an
6
- # answer while a verifier agent evaluates its quality.
7
- #
8
- # @see https://claude.com/blog/multi-agent-coordination-patterns
9
- #
10
- # All prompt construction and result parsing are provided by the caller,
11
- # giving full control over the LLM dialogue.
12
- # The generator and verifier agents are configurable, and the pipeline
13
- # retries until confidence passes the threshold or max iterations are reached.
14
- #
15
- # @example Basic usage with custom prompt builders
16
- # pipeline = Phronomy::GeneratorVerifier.new(
17
- # draft_agent: MyDraftAgent,
18
- # review_agent: MyReviewAgent,
19
- # draft_prompt_builder: ->(input, feedback) { "Question: #{input}" },
20
- # review_prompt_builder: ->(input, draft, citations) { "Review: #{draft}" }
21
- # )
22
- # result = pipeline.invoke("What is the refund policy?")
23
- # puts result.output # the final answer string
24
- # puts result.trusted? # true when confidence >= threshold
6
+ # Implements the Generator-Verifier multi-agent coordination pattern.
25
7
  #
26
- # @example Custom result parsers
27
- # pipeline = Phronomy::GeneratorVerifier.new(
28
- # ...,
29
- # draft_result_parser: ->(text) { my_parse_draft(text) },
30
- # review_result_parser: ->(text) { my_parse_review(text) }
31
- # )
32
- #
33
- # @example Raising on low confidence
34
- # pipeline = Phronomy::GeneratorVerifier.new(
35
- # ...,
36
- # raise_if_untrusted: true
37
- # )
38
- # begin
39
- # result = pipeline.invoke("question")
40
- # rescue Phronomy::LowConfidenceError => e
41
- # puts "Untrusted: #{e.result.confidence}"
42
- # end
8
+ # Agent completion is integrated through application-defined Workflow events;
9
+ # Workflow entry actions do not return or await Agent Tasks.
43
10
  class GeneratorVerifier
44
- # Default confidence threshold for trusting an answer.
45
11
  DEFAULT_CONFIDENCE_THRESHOLD = 0.7
46
-
47
- # Default maximum draft-review cycles before returning best effort.
48
12
  DEFAULT_MAX_ITERATIONS = 3
49
13
 
50
- # Immutable value object returned by {GeneratorVerifier#invoke}.
51
- #
52
- # @!attribute [r] output
53
- # @return [String] the final answer text
54
- # @!attribute [r] confidence
55
- # @return [Float] combined confidence score (0.0–1.0)
56
- # @!attribute [r] citations
57
- # @return [Array<Hash>] [{source:, excerpt:}, ...]
58
- #
59
- # **WARNING**: These citations are extracted from the LLM's own response
60
- # and are **not** verified against any external knowledge base or URL.
61
- # Do not treat them as authoritative without independent verification.
62
- # @!attribute [r] iterations
63
- # @return [Integer] number of draft-review cycles executed
64
- # @!attribute [r] review_notes
65
- # @return [Array<String>] reviewer feedback for each cycle
66
- # @!attribute [r] trusted
67
- # @return [Boolean] true when confidence >= threshold
68
14
  Result = Struct.new(
69
- :output, :confidence, :citations, :iterations, :review_notes, :trusted
15
+ :output,
16
+ :confidence,
17
+ :citations,
18
+ :iterations,
19
+ :review_notes,
20
+ :trusted
70
21
  ) do
71
- # @return [Boolean] true when confidence >= threshold
72
22
  alias_method :trusted?, :trusted
73
23
  end
74
24
 
75
- # Internal graph state — not part of the public API.
76
- # @private
77
25
  class PipelineState
78
26
  include Phronomy::WorkflowContext
79
27
 
80
28
  field :input, type: :replace, default: -> { "" }
81
- field :draft, type: :replace, default: -> {}
29
+ field :draft, type: :replace, default: -> { {} }
82
30
  field :self_score, type: :replace, default: -> { 0.0 }
83
31
  field :review_score, type: :replace, default: -> { 0.0 }
84
32
  field :citations, type: :replace, default: -> { [] }
85
33
  field :review_notes, type: :append, default: -> { [] }
86
34
  field :iteration, type: :replace, default: -> { 0 }
87
35
  field :approved, type: :replace, default: -> { false }
88
- field :output, type: :replace, default: -> {}
36
+ field :output, type: :replace, default: -> { {} }
37
+ field :draft_request_id, type: :replace, default: nil
38
+ field :review_request_id, type: :replace, default: nil
39
+ field :pipeline_error, type: :replace, default: nil
40
+
41
+ # Application-level event interpretation. Correlation IDs belong to this
42
+ # Pipeline rather than to the generic FSMSession.
43
+ def handle_fsm_event(event)
44
+ case event.type
45
+ when :draft_completed
46
+ return :consume unless event.payload[:request_id] == draft_request_id
47
+
48
+ self.draft = event.payload[:draft]
49
+ self.self_score = event.payload[:self_score]
50
+ self.citations = event.payload[:citations]
51
+ self.iteration = iteration + 1
52
+ self.draft_request_id = nil
53
+ self.pipeline_error = nil
54
+ when :review_completed
55
+ return :consume unless event.payload[:request_id] == review_request_id
56
+
57
+ self.review_score = event.payload[:review_score]
58
+ self.approved = event.payload[:approved]
59
+ self.review_notes = review_notes + [event.payload[:feedback]]
60
+ self.review_request_id = nil
61
+ self.pipeline_error = nil
62
+ when :draft_failed
63
+ return :consume unless event.payload[:request_id] == draft_request_id
64
+
65
+ self.pipeline_error = event.payload[:error]
66
+ self.draft_request_id = nil
67
+ when :review_failed
68
+ return :consume unless event.payload[:request_id] == review_request_id
69
+
70
+ self.pipeline_error = event.payload[:error]
71
+ self.review_request_id = nil
72
+ end
73
+ false
74
+ end
89
75
  end
90
76
 
91
77
  private_constant :PipelineState
92
78
 
93
- # @param draft_agent [Class] subclass of Phronomy::Agent::Base
94
- # used to generate answer drafts
95
- # @param review_agent [Class] subclass of Phronomy::Agent::Base
96
- # used to evaluate each draft
97
- # @param draft_prompt_builder [#call] +call(input, feedback)+ → String
98
- # prompt for the generator. +feedback+ is nil on the first iteration and
99
- # contains the reviewer's feedback string on subsequent iterations.
100
- # @param review_prompt_builder [#call] +call(input, draft, citations)+ → String
101
- # prompt for the verifier. +citations+ is an Array of Hashes.
102
- # @param draft_result_parser [#call, nil] +call(text)+ → Hash with
103
- # +:answer+, +:confidence+, and +:citations+ keys. Defaults to JSON parsing
104
- # with a safe fallback when the response cannot be parsed.
105
- # @param review_result_parser [#call, nil] +call(text)+ → Hash with
106
- # +:approved+, +:score+, and +:feedback+ keys. Defaults to JSON parsing
107
- # with a safe fallback.
108
- # @param confidence_threshold [Float] minimum combined confidence to
109
- # trust an answer (default: 0.7)
110
- # @param max_iterations [Integer] maximum draft-review cycles
111
- # before returning the best-effort answer (default: 3)
112
- # @param raise_if_untrusted [Boolean] when +true+, raises
113
- # {Phronomy::LowConfidenceError} if the final result does not meet the
114
- # confidence threshold (default: false)
115
- # @api private
116
79
  def initialize(
117
80
  draft_agent:,
118
81
  review_agent:,
@@ -128,25 +91,18 @@ module Phronomy
128
91
  @review_agent_class = review_agent
129
92
  @draft_prompt_builder = draft_prompt_builder
130
93
  @review_prompt_builder = review_prompt_builder
131
- @draft_result_parser = draft_result_parser || method(:default_parse_draft)
132
- @review_result_parser = review_result_parser || method(:default_parse_review)
94
+ @draft_result_parser =
95
+ draft_result_parser || method(:default_parse_draft)
96
+ @review_result_parser =
97
+ review_result_parser || method(:default_parse_review)
133
98
  @threshold = confidence_threshold.to_f
134
99
  @max_iterations = max_iterations.to_i
135
100
  @raise_if_untrusted = raise_if_untrusted
136
101
  @compiled_workflow = nil
137
102
  end
138
103
 
139
- # Run the generator-verifier pipeline.
140
- #
141
- # @param input [String] the user question or task description
142
- # @param config [Hash] forwarded to the underlying agents (e.g. thread_id)
143
- # @return [Result]
144
- # @raise [Phronomy::LowConfidenceError] when +raise_if_untrusted:+ is +true+
145
- # and the result does not meet the confidence threshold
146
- # @api private
147
104
  def invoke(input, config: {})
148
- app = compiled_workflow
149
- state = app.invoke({input: input}, config: config)
105
+ state = compiled_workflow.invoke({input: input}, config: config)
150
106
  confidence = combined_confidence(state)
151
107
  trusted = confidence >= @threshold
152
108
  result = Result.new(
@@ -157,14 +113,19 @@ module Phronomy
157
113
  review_notes: state.review_notes,
158
114
  trusted: trusted
159
115
  )
160
- raise LowConfidenceError.new(result) if @raise_if_untrusted && !trusted
116
+ if @raise_if_untrusted && !trusted
117
+ raise LowConfidenceError.new(result)
118
+ end
161
119
  result
162
120
  end
163
121
 
164
122
  private
165
123
 
166
124
  def combined_confidence(state)
167
- [(state.self_score || 0.0).to_f, (state.review_score || 0.0).to_f].min
125
+ [
126
+ (state.self_score || 0.0).to_f,
127
+ (state.review_score || 0.0).to_f
128
+ ].min
168
129
  end
169
130
 
170
131
  def compiled_workflow
@@ -175,86 +136,236 @@ module Phronomy
175
136
  draft_agent = @draft_agent_class.new
176
137
  review_agent = @review_agent_class.new
177
138
  threshold = @threshold
178
- max_iter = @max_iterations
179
- dpb = @draft_prompt_builder
180
- rpb = @review_prompt_builder
181
- drp = @draft_result_parser
182
- rrp = @review_result_parser
139
+ max_iterations = @max_iterations
140
+ draft_prompt_builder = @draft_prompt_builder
141
+ review_prompt_builder = @review_prompt_builder
142
+ draft_result_parser = @draft_result_parser
143
+ review_result_parser = @review_result_parser
183
144
  pipeline = self
145
+ workflow = nil
184
146
 
185
- Phronomy::Workflow.define(PipelineState) do
147
+ # workflow is assigned after define so closures captured by reference see the result.
148
+ workflow = Phronomy::Workflow.define(PipelineState) do
186
149
  initial :draft
187
150
 
188
151
  state :draft
189
152
  state :review
190
153
  state :finalize
154
+ state :failed
191
155
 
192
156
  entry :draft, ->(state) {
193
- feedback = state.review_notes.last
194
- prompt = dpb.call(state.input, feedback)
195
- # Return a Task so WorkflowRunner awaits it off the EventLoop thread,
196
- # then posts :action_completed with the new context back to EventLoop.
197
- draft_agent.invoke_async(prompt).map do |result|
198
- parsed = drp.call(result[:output])
199
- state.merge(
200
- draft: parsed[:answer].to_s,
201
- self_score: pipeline.__send__(:clamp, parsed[:confidence]),
202
- citations: pipeline.__send__(:normalize_citations, parsed[:citations]),
203
- iteration: state.iteration + 1
204
- )
205
- end
157
+ request_id = SecureRandom.uuid
158
+ next_state = state.merge(draft_request_id: request_id)
159
+ feedback = next_state.review_notes.last
160
+ prompt = draft_prompt_builder.call(next_state.input, feedback)
161
+
162
+ draft_agent.invoke_async(
163
+ prompt,
164
+ on_event: ->(agent_event) {
165
+ case agent_event.type
166
+ when :done
167
+ begin
168
+ parsed = draft_result_parser.call(
169
+ agent_event.payload[:output]
170
+ )
171
+ workflow.signal(
172
+ thread_id: next_state.thread_id,
173
+ event: :draft_completed,
174
+ payload: {
175
+ request_id: request_id,
176
+ draft: parsed[:answer].to_s,
177
+ self_score: pipeline.__send__(
178
+ :clamp,
179
+ parsed[:confidence]
180
+ ),
181
+ citations: pipeline.__send__(
182
+ :normalize_citations,
183
+ parsed[:citations]
184
+ )
185
+ }
186
+ )
187
+ rescue => error
188
+ workflow.signal(
189
+ thread_id: next_state.thread_id,
190
+ event: :draft_failed,
191
+ payload: {
192
+ request_id: request_id,
193
+ error: error
194
+ }
195
+ )
196
+ end
197
+ when :error, :timeout, :cancelled
198
+ workflow.signal(
199
+ thread_id: next_state.thread_id,
200
+ event: :draft_failed,
201
+ payload: {
202
+ request_id: request_id,
203
+ error:
204
+ agent_event.payload[:error] ||
205
+ Phronomy::Error.new(
206
+ "Draft Agent ended with #{agent_event.type}"
207
+ )
208
+ }
209
+ )
210
+ when :approval_required
211
+ workflow.signal(
212
+ thread_id: next_state.thread_id,
213
+ event: :draft_failed,
214
+ payload: {
215
+ request_id: request_id,
216
+ error: Phronomy::Error.new(
217
+ "GeneratorVerifier draft Agent suspended for approval"
218
+ )
219
+ }
220
+ )
221
+ end
222
+ }
223
+ )
224
+
225
+ next_state
206
226
  }
207
227
 
208
228
  entry :review, ->(state) {
209
- prompt = rpb.call(state.input, state.draft, state.citations)
210
- # Return a Task so WorkflowRunner awaits it off the EventLoop thread.
211
- review_agent.invoke_async(prompt).map do |result|
212
- parsed = rrp.call(result[:output])
213
- state.merge(
214
- review_score: pipeline.__send__(:clamp, parsed[:score]),
215
- approved: parsed[:approved] == true,
216
- review_notes: [parsed[:feedback].to_s]
217
- )
218
- end
229
+ request_id = SecureRandom.uuid
230
+ next_state = state.merge(review_request_id: request_id)
231
+ prompt = review_prompt_builder.call(
232
+ next_state.input,
233
+ next_state.draft,
234
+ next_state.citations
235
+ )
236
+
237
+ review_agent.invoke_async(
238
+ prompt,
239
+ on_event: ->(agent_event) {
240
+ case agent_event.type
241
+ when :done
242
+ begin
243
+ parsed = review_result_parser.call(
244
+ agent_event.payload[:output]
245
+ )
246
+ workflow.signal(
247
+ thread_id: next_state.thread_id,
248
+ event: :review_completed,
249
+ payload: {
250
+ request_id: request_id,
251
+ review_score: pipeline.__send__(
252
+ :clamp,
253
+ parsed[:score]
254
+ ),
255
+ approved: parsed[:approved] == true,
256
+ feedback: parsed[:feedback].to_s
257
+ }
258
+ )
259
+ rescue => error
260
+ workflow.signal(
261
+ thread_id: next_state.thread_id,
262
+ event: :review_failed,
263
+ payload: {
264
+ request_id: request_id,
265
+ error: error
266
+ }
267
+ )
268
+ end
269
+ when :error, :timeout, :cancelled
270
+ workflow.signal(
271
+ thread_id: next_state.thread_id,
272
+ event: :review_failed,
273
+ payload: {
274
+ request_id: request_id,
275
+ error:
276
+ agent_event.payload[:error] ||
277
+ Phronomy::Error.new(
278
+ "Review Agent ended with #{agent_event.type}"
279
+ )
280
+ }
281
+ )
282
+ when :approval_required
283
+ workflow.signal(
284
+ thread_id: next_state.thread_id,
285
+ event: :review_failed,
286
+ payload: {
287
+ request_id: request_id,
288
+ error: Phronomy::Error.new(
289
+ "GeneratorVerifier review Agent suspended for approval"
290
+ )
291
+ }
292
+ )
293
+ end
294
+ }
295
+ )
296
+
297
+ next_state
219
298
  }
220
299
 
221
- entry :finalize, ->(state) { state.output = state.draft }
300
+ entry :finalize, ->(state) {
301
+ state.output = state.draft
302
+ state
303
+ }
222
304
 
223
- transition from: :draft, to: :review
224
- transition from: :finalize, to: :__finish__
305
+ entry :failed, ->(state) {
306
+ raise(
307
+ state.pipeline_error ||
308
+ Phronomy::Error.new("GeneratorVerifier Agent operation failed")
309
+ )
310
+ }
311
+
312
+ transition from: :draft, on: :draft_completed, to: :review
313
+ transition from: :draft, on: :draft_failed, to: :failed
225
314
 
226
315
  transition from: :review,
316
+ on: :review_completed,
227
317
  guard: ->(state) {
228
- confidence = [state.self_score || 0.0, state.review_score || 0.0].min
229
- (confidence >= threshold && state.approved) || state.iteration >= max_iter
318
+ confidence = [
319
+ state.self_score || 0.0,
320
+ state.review_score || 0.0
321
+ ].min
322
+ (confidence >= threshold && state.approved) ||
323
+ state.iteration >= max_iterations
230
324
  },
231
325
  to: :finalize
232
- transition from: :review, to: :draft
326
+ transition from: :review,
327
+ on: :review_completed,
328
+ to: :draft
329
+ transition from: :review,
330
+ on: :review_failed,
331
+ to: :failed
332
+
333
+ transition from: :finalize, to: :__finish__
233
334
  end
234
335
  end
235
336
 
236
337
  def default_parse_draft(text)
237
338
  json_parser.parse(text)
238
339
  rescue Phronomy::ParseError
239
- {answer: text.to_s, confidence: 0.0, citations: []}
340
+ {
341
+ answer: text.to_s,
342
+ confidence: 0.0,
343
+ citations: []
344
+ }
240
345
  end
241
346
 
242
347
  def default_parse_review(text)
243
348
  json_parser.parse(text)
244
349
  rescue Phronomy::ParseError
245
- {approved: false, score: 0.0, feedback: "Review output could not be parsed: #{text}"}
350
+ {
351
+ approved: false,
352
+ score: 0.0,
353
+ feedback: "Review output could not be parsed: #{text}"
354
+ }
246
355
  end
247
356
 
248
357
  def json_parser
249
358
  @json_parser ||= Phronomy::OutputParser::JsonParser.new
250
359
  end
251
360
 
252
- def clamp(val)
253
- val.to_f.clamp(0.0, 1.0)
361
+ def clamp(value)
362
+ value.to_f.clamp(0.0, 1.0)
254
363
  end
255
364
 
256
365
  def normalize_citations(raw)
257
- Array(raw).filter_map { |c| c.is_a?(Hash) ? c.transform_keys(&:to_sym) : nil }
366
+ Array(raw).filter_map do |citation|
367
+ citation.is_a?(Hash) ? citation.transform_keys(&:to_sym) : nil
368
+ end
258
369
  end
259
370
  end
260
371
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Raised when a synchronous FSM entry action returns Phronomy::Task.
5
+ #
6
+ # Entry actions are Run-to-Completion callbacks. They may start asynchronous
7
+ # work, but completion must return through a later explicit event.
8
+ class InvalidAsyncEntryActionError < InvalidAsyncWorkflowActionError; end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Raised when a synchronous Workflow transition action returns Phronomy::Task.
5
+ #
6
+ # Transition actions are Run-to-Completion callbacks. They may start
7
+ # asynchronous work, but completion must return through a later explicit event.
8
+ class InvalidAsyncTransitionActionError <
9
+ InvalidAsyncWorkflowActionError
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Base error for synchronous Workflow callbacks that return Phronomy::Task.
5
+ #
6
+ # Workflow callbacks may start asynchronous work, but they must return
7
+ # synchronously and deliver completion through a later explicit event.
8
+ class InvalidAsyncWorkflowActionError < Error; end
9
+ end
@@ -10,9 +10,8 @@ module Phronomy
10
10
  #
11
11
  # @example Build a context for a new agent invocation
12
12
  # ctx = Phronomy::InvocationContext.new(
13
- # thread_id: "conv-123",
14
- # cancellation_token: Phronomy::Concurrency::CancellationToken.timeout_after(30),
15
- # max_parallel_tools: 5
13
+ # thread_id: "conv-123",
14
+ # cancellation_token: Phronomy::Concurrency::CancellationToken.timeout_after(30)
16
15
  # )
17
16
  # agent.invoke("Hello", invocation_context: ctx)
18
17
  class InvocationContext
@@ -37,18 +36,13 @@ module Phronomy
37
36
  # @return [Integer, nil] max tokens the agent may consume this invocation
38
37
  attr_reader :token_budget
39
38
 
40
- # @return [Integer] maximum simultaneous tool calls (default: 10)
41
- attr_reader :max_parallel_tools
42
-
43
- # @return [Object, nil] approval policy applied before write-scope tools
39
+ # @return [#call, nil] invocation-specific Tool approval policy. The callable
40
+ # receives Phronomy::Agent::ApprovalEvaluationRequest.
44
41
  attr_reader :approval_policy
45
42
 
46
43
  # @return [Object, nil] redaction policy applied to tool args / results
47
44
  attr_reader :redaction_policy
48
45
 
49
- # @return [Hash, nil] per-provider concurrency / rate-limit overrides
50
- attr_reader :provider_limits
51
-
52
46
  # @return [String, nil] unique identifier for this task in the trace tree
53
47
  attr_reader :task_id
54
48
 
@@ -62,10 +56,8 @@ module Phronomy
62
56
  # @param deadline [Deadline, nil]
63
57
  # @param tracer_span [Object, nil]
64
58
  # @param token_budget [Integer, nil]
65
- # @param max_parallel_tools [Integer]
66
- # @param approval_policy [Object, nil]
59
+ # @param approval_policy [#call, nil] invocation-specific Tool approval policy
67
60
  # @param redaction_policy [Object, nil]
68
- # @param provider_limits [Hash, nil]
69
61
  # @param task_id [String, nil]
70
62
  # @param parent_task_id [String, nil]
71
63
  # @api private
@@ -77,10 +69,8 @@ module Phronomy
77
69
  deadline: nil,
78
70
  tracer_span: nil,
79
71
  token_budget: nil,
80
- max_parallel_tools: 10,
81
72
  approval_policy: nil,
82
73
  redaction_policy: nil,
83
- provider_limits: nil,
84
74
  task_id: nil,
85
75
  parent_task_id: nil
86
76
  )
@@ -91,10 +81,8 @@ module Phronomy
91
81
  @deadline = deadline
92
82
  @tracer_span = tracer_span
93
83
  @token_budget = token_budget
94
- @max_parallel_tools = max_parallel_tools
95
84
  @approval_policy = approval_policy
96
85
  @redaction_policy = redaction_policy
97
- @provider_limits = provider_limits
98
86
  @task_id = task_id
99
87
  @parent_task_id = parent_task_id
100
88
  end
@@ -114,10 +102,8 @@ module Phronomy
114
102
  deadline: overrides.fetch(:deadline, @deadline),
115
103
  tracer_span: overrides.fetch(:tracer_span, @tracer_span),
116
104
  token_budget: overrides.fetch(:token_budget, @token_budget),
117
- max_parallel_tools: overrides.fetch(:max_parallel_tools, @max_parallel_tools),
118
105
  approval_policy: overrides.fetch(:approval_policy, @approval_policy),
119
106
  redaction_policy: overrides.fetch(:redaction_policy, @redaction_policy),
120
- provider_limits: overrides.fetch(:provider_limits, @provider_limits),
121
107
  task_id: overrides.fetch(:task_id, @task_id),
122
108
  parent_task_id: overrides.fetch(:parent_task_id, @parent_task_id)
123
109
  )