phronomy 0.15.1 → 0.16.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -28
  3. data/README.md +300 -75
  4. data/benchmark/bench_agent_invoke.rb +3 -0
  5. data/benchmark/bench_regression.rb +2 -18
  6. data/benchmark/bench_tool_schema.rb +1 -0
  7. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
  8. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
  9. data/lib/phronomy/agent/activation_registry.rb +28 -0
  10. data/lib/phronomy/agent/agent_execution.rb +97 -0
  11. data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
  12. data/lib/phronomy/agent/agent_invocation.rb +42 -10
  13. data/lib/phronomy/agent/agent_invocation_session_builder.rb +50 -11
  14. data/lib/phronomy/agent/agent_root.rb +67 -0
  15. data/lib/phronomy/agent/async_event_api.rb +55 -475
  16. data/lib/phronomy/agent/base.rb +301 -285
  17. data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
  18. data/lib/phronomy/agent/context_assembler.rb +321 -0
  19. data/lib/phronomy/agent/context_candidate.rb +47 -0
  20. data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
  21. data/lib/phronomy/agent/context_importer.rb +217 -0
  22. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
  23. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
  24. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
  25. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +188 -0
  26. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
  27. data/lib/phronomy/agent/context_plan.rb +25 -0
  28. data/lib/phronomy/agent/context_plan_validator.rb +167 -0
  29. data/lib/phronomy/agent/context_policies/default.rb +53 -0
  30. data/lib/phronomy/agent/context_policy.rb +15 -0
  31. data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
  32. data/lib/phronomy/agent/context_policy_registry.rb +46 -0
  33. data/lib/phronomy/agent/context_request.rb +35 -0
  34. data/lib/phronomy/agent/context_selection_unit.rb +38 -0
  35. data/lib/phronomy/agent/derived_content_spec.rb +34 -0
  36. data/lib/phronomy/agent/execution_coordinator.rb +1123 -0
  37. data/lib/phronomy/agent/fsm_runtime_adapter.rb +210 -0
  38. data/lib/phronomy/agent/immutable.rb +31 -0
  39. data/lib/phronomy/agent/journal_projection.rb +34 -0
  40. data/lib/phronomy/agent/journal_record.rb +67 -0
  41. data/lib/phronomy/agent/llm_call_record.rb +51 -0
  42. data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
  43. data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
  44. data/lib/phronomy/agent/llm_input_patch.rb +21 -0
  45. data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
  46. data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
  47. data/lib/phronomy/agent/ruby_llm_materializer.rb +298 -0
  48. data/lib/phronomy/agent/token_budget_resolver.rb +69 -0
  49. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
  50. data/lib/phronomy/agent/tool_definition_set.rb +55 -0
  51. data/lib/phronomy/agent.rb +14 -16
  52. data/lib/phronomy/agent_busy_error.rb +5 -0
  53. data/lib/phronomy/canonical_json.rb +136 -0
  54. data/lib/phronomy/configuration.rb +9 -4
  55. data/lib/phronomy/content_store/base.rb +51 -0
  56. data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
  57. data/lib/phronomy/engine/event_loop.rb +3 -0
  58. data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
  59. data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
  60. data/lib/phronomy/llm_context_window/assembler.rb +8 -8
  61. data/lib/phronomy/multi_agent/orchestrator.rb +1 -0
  62. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +6 -2
  64. data/lib/phronomy/persistence/in_memory.rb +247 -0
  65. data/lib/phronomy/persistence.rb +39 -0
  66. data/lib/phronomy/tools/agent.rb +14 -36
  67. data/lib/phronomy/version.rb +1 -1
  68. data/lib/phronomy.rb +11 -0
  69. data/scripts/add_to_h_to_token_doubles.rb +33 -0
  70. data/scripts/add_to_h_unnamed_doubles.rb +27 -0
  71. data/scripts/migrate_spec_agent_definition.rb +108 -0
  72. data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
  73. data/scripts/migrate_spec_inline_pass3.rb +24 -0
  74. metadata +54 -7
  75. data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
  76. data/lib/phronomy/agent/before_completion_context.rb +0 -47
  77. data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
@@ -2,55 +2,17 @@
2
2
 
3
3
  module Phronomy
4
4
  module Agent
5
- # Symmetric Agent async event contract layered onto Agent::Base.
6
- #
7
- # invoke_async and stream_async share lifecycle/tool events. stream_async
8
- # additionally emits :token events. The returned Task remains a normal Task
9
- # and is settled after the terminal event listener returns.
10
- #
11
- # @api private
5
+ # Public Agent execution API. Agent instances are always stateful and all
6
+ # operations are coordinated through Phronomy::Persistence.
12
7
  module AsyncEventApi
13
- # Invokes the agent synchronously and returns the terminal result.
14
- #
15
- # Provider errors are translated after the configured LLM adapter returns
16
- # its final result. Phronomy does not replay the Agent invocation.
17
- #
18
- # @param input [String, Hash] user input for this invocation
19
- # @param messages [Array<RubyLLM::Message>] conversation history
20
- # @param thread_id [String, nil] conversation thread identifier
21
- # @param config [Hash] additional runtime options
22
- # @param invocation_context [Phronomy::InvocationContext, nil]
23
- # first-class invocation context
24
- # @param on_event [Proc, nil] listener for lifecycle and Tool events
25
- # @return [Hash] terminal invocation result
26
- # @raise [Phronomy::SchedulerReentrancyError] when called from the
27
- # EventLoop thread or a guarded scheduler context
28
- # @api public
29
- def invoke(
30
- input,
31
- messages: [],
32
- thread_id: nil,
33
- config: {},
34
- invocation_context: nil,
35
- on_event: nil
36
- )
8
+ def invoke(input, thread_id: nil, config: {}, invocation_context: nil, on_event: nil)
37
9
  if invocation_context
38
- thread_id, config = _apply_invocation_context(
39
- thread_id,
40
- config,
41
- invocation_context
42
- )
10
+ thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
43
11
  end
44
12
  _check_scheduler_reentrancy(:invoke, :invoke_async)
45
-
46
- trace(
47
- "agent.invoke",
48
- input: input,
49
- **_build_caller_meta(config)
50
- ) do |_span|
13
+ trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
51
14
  result = invoke_async(
52
15
  input,
53
- messages: messages,
54
16
  thread_id: thread_id,
55
17
  config: config,
56
18
  on_event: on_event
@@ -59,26 +21,8 @@ module Phronomy
59
21
  end
60
22
  end
61
23
 
62
- # Invokes the agent asynchronously.
63
- #
64
- # The returned Task settles after the terminal event listener returns.
65
- # Lifecycle and Tool events are delivered from the Runtime-owned
66
- # EventLoop thread.
67
- #
68
- # @param input [String, Hash] user input for this invocation
69
- # @param messages [Array<RubyLLM::Message>] conversation history
70
- # @param thread_id [String, nil] conversation thread identifier
71
- # @param config [Hash] additional runtime options
72
- # @param invocation_context [Phronomy::InvocationContext, nil]
73
- # first-class invocation context
74
- # @param on_tool_approval_required [Proc, nil] per-invocation approval
75
- # notification listener
76
- # @param on_event [Proc, nil] listener for lifecycle and Tool events
77
- # @return [Phronomy::Task] Task resolving to the terminal result
78
- # @api public
79
24
  def invoke_async(
80
25
  input,
81
- messages: [],
82
26
  thread_id: nil,
83
27
  config: {},
84
28
  invocation_context: nil,
@@ -86,123 +30,22 @@ module Phronomy
86
30
  on_event: nil
87
31
  )
88
32
  if invocation_context
89
- thread_id, config = _apply_invocation_context(
90
- thread_id,
91
- config,
92
- invocation_context
93
- )
33
+ thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
94
34
  end
95
-
96
- result_task = Phronomy::Task.deferred(
97
- name:
98
- "agent-#{(self.class.name || "anonymous").downcase}-async"
99
- )
100
- approval_snapshot = _approval_configuration_snapshot(
101
- on_tool_approval_required
102
- )
103
- _start_invocation(
104
- result_task,
35
+ approval = _approval_configuration_snapshot(on_tool_approval_required)
36
+ execution_coordinator.start(
105
37
  input,
106
- messages: messages,
107
38
  thread_id: thread_id,
108
39
  config: config,
109
- approval_snapshot: approval_snapshot,
110
40
  mode: :invoke,
41
+ approval_policy: approval[:policy],
42
+ approval_listener: approval[:listener],
111
43
  on_event: on_event
112
44
  )
113
- result_task
114
45
  end
115
46
 
116
- # Invokes the agent asynchronously and emits streaming events.
117
- #
118
- # Accepts either +on_event:+ or a block. Supplying both is rejected.
119
- # Token events are emitted only by streaming invocations; lifecycle and
120
- # Tool events use the same contract as {#invoke_async}.
121
- #
122
- # @param input [String, Hash] user input for this invocation
123
- # @param messages [Array<RubyLLM::Message>] conversation history
124
- # @param thread_id [String, nil] conversation thread identifier
125
- # @param config [Hash] additional runtime options
126
- # @param invocation_context [Phronomy::InvocationContext, nil]
127
- # first-class invocation context
128
- # @param on_tool_approval_required [Proc, nil] per-invocation approval
129
- # notification listener
130
- # @param on_event [Proc, nil] event listener
131
- # @yieldparam event [Phronomy::Agent::StreamEvent]
132
- # @return [Phronomy::Task] Task resolving to the terminal result
133
- # @raise [ArgumentError] when no listener is supplied or both listener
134
- # forms are supplied
135
- # @api public
136
- def stream_async(
137
- input,
138
- messages: [],
139
- thread_id: nil,
140
- config: {},
141
- invocation_context: nil,
142
- on_tool_approval_required: nil,
143
- on_event: nil,
144
- &block
145
- )
146
- listener = resolve_event_listener(on_event, block)
147
- unless listener
148
- raise ArgumentError,
149
- "stream_async requires on_event: or a block"
150
- end
151
-
152
- if invocation_context
153
- thread_id, config = _apply_invocation_context(
154
- thread_id,
155
- config,
156
- invocation_context
157
- )
158
- end
159
-
160
- result_task = Phronomy::Task.deferred(
161
- name:
162
- "agent-#{(self.class.name || "anonymous").downcase}" \
163
- "-stream-async"
164
- )
165
- approval_snapshot = _approval_configuration_snapshot(
166
- on_tool_approval_required
167
- )
168
- _start_invocation(
169
- result_task,
170
- input,
171
- messages: messages,
172
- thread_id: thread_id,
173
- config: config,
174
- approval_snapshot: approval_snapshot,
175
- mode: :stream,
176
- on_event: listener
177
- )
178
- result_task
179
- end
180
-
181
- # Invokes the agent synchronously and emits streaming events.
182
- #
183
- # Accepts either +on_event:+ or a block. Event callbacks run on the
184
- # Runtime-owned EventLoop thread; the calling thread waits for the final
185
- # Task result.
186
- #
187
- # @param input [String, Hash] user input for this invocation
188
- # @param messages [Array<RubyLLM::Message>] conversation history
189
- # @param thread_id [String, nil] conversation thread identifier
190
- # @param config [Hash] additional runtime options
191
- # @param invocation_context [Phronomy::InvocationContext, nil]
192
- # first-class invocation context
193
- # @param on_tool_approval_required [Proc, nil] per-invocation approval
194
- # notification listener
195
- # @param on_event [Proc, nil] event listener
196
- # @yieldparam event [Phronomy::Agent::StreamEvent]
197
- # @return [Hash] terminal invocation result
198
- # @raise [ArgumentError] when no listener is supplied or both listener
199
- # forms are supplied
200
- # @raise [Phronomy::SchedulerReentrancyError] when called from the
201
- # EventLoop thread or a guarded scheduler context
202
- # @api public
203
47
  def stream(
204
48
  input,
205
- messages: [],
206
49
  thread_id: nil,
207
50
  config: {},
208
51
  invocation_context: nil,
@@ -211,342 +54,79 @@ module Phronomy
211
54
  &block
212
55
  )
213
56
  listener = resolve_event_listener(on_event, block)
214
- unless listener
215
- raise ArgumentError,
216
- "stream requires on_event: or a block"
217
- end
218
-
57
+ raise ArgumentError, "stream requires on_event: or a block" unless listener
219
58
  if invocation_context
220
- thread_id, config = _apply_invocation_context(
221
- thread_id,
222
- config,
223
- invocation_context
224
- )
59
+ thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
225
60
  end
226
61
  _check_scheduler_reentrancy(:stream, :stream_async)
227
-
228
- trace(
229
- "agent.stream",
230
- input: input,
231
- **_build_caller_meta(config)
232
- ) do |_span|
62
+ trace("agent.stream", input: input, **_build_caller_meta(config)) do |_span|
233
63
  result = stream_async(
234
64
  input,
235
- messages: messages,
236
65
  thread_id: thread_id,
237
66
  config: config,
238
- on_tool_approval_required:
239
- on_tool_approval_required,
67
+ on_tool_approval_required: on_tool_approval_required,
240
68
  on_event: listener
241
69
  ).wait_result
242
70
  [result, result[:usage]]
243
71
  end
244
72
  end
245
73
 
246
- private
247
-
248
- def resolve_event_listener(keyword_listener, block_listener)
249
- if keyword_listener && block_listener
250
- raise ArgumentError,
251
- "Provide either on_event: or a block, not both"
252
- end
253
- keyword_listener || block_listener
254
- end
255
-
256
- # Installs the Application listener before EventLoop admission.
257
- # Cancellation is checked by the first Agent entry action on the EventLoop
258
- # thread so even pre-cancelled invocations produce a terminal event before
259
- # the returned Task settles.
260
- # @api private
261
- def _start_invocation(
262
- result_task,
74
+ def stream_async(
263
75
  input,
264
- messages:,
265
- thread_id:,
266
- config:,
267
- approval_snapshot:,
268
- mode: :invoke,
269
- on_event: nil
270
- )
271
- effective_config =
272
- thread_id ? config.merge(thread_id: thread_id) : config
273
- runtime = Phronomy::Runtime.instance
274
- event_loop = runtime.event_loop
275
- session = Agent::AgentInvocationSessionBuilder.build(
276
- agent: self,
277
- input: input,
278
- messages: messages,
279
- config: effective_config,
280
- approval_policy: approval_snapshot[:policy],
281
- approval_listener: approval_snapshot[:listener],
282
- mode: mode,
283
- on_event: on_event,
284
- runtime: runtime
285
- )
286
- callback_error_policy =
287
- Phronomy.configuration.stream_callback_error_policy
288
- source_task = Phronomy::Task.deferred(
289
- name: "#{result_task.name}-source"
290
- )
291
- source_task.on_complete do |invocation, error|
292
- completed_invocation = invocation || session.context
293
- _handle_agent_completion(
294
- result_task: result_task,
295
- invocation: completed_invocation,
296
- error: error,
297
- mode: mode,
298
- listener: on_event,
299
- event_loop: event_loop,
300
- callback_error_policy: callback_error_policy
301
- )
302
- end
303
-
304
- event_loop.register(session, completion: source_task)
305
- rescue => error
306
- _fail_result_task(result_task, error)
307
- end
308
-
309
- def _handle_agent_completion(
310
- result_task:,
311
- invocation:,
312
- error:,
313
- mode:,
314
- listener:,
315
- event_loop:,
316
- callback_error_policy:
76
+ thread_id: nil,
77
+ config: {},
78
+ invocation_context: nil,
79
+ on_tool_approval_required: nil,
80
+ on_event: nil,
81
+ &block
317
82
  )
318
- if listener && !event_loop.current?
319
- completion_error = error || Phronomy::Error.new(
320
- "Agent event delivery occurred outside the EventLoop"
321
- )
322
- _fail_result_task(
323
- result_task,
324
- _translated_error(completion_error)
325
- )
326
- return
327
- end
328
-
329
- result = nil
330
- execution_error = nil
331
- begin
332
- raise error if error
333
-
334
- result = _extract_invoke_result(invocation)
335
- rescue => caught
336
- execution_error = _translated_error(caught)
337
- end
338
- if execution_error
339
- execution_error = normalize_terminal_error(
340
- execution_error,
341
- invocation
342
- )
343
- end
344
-
345
- if execution_error
346
- terminal_event = StreamEvent.new(
347
- type: terminal_error_event_type(execution_error),
348
- payload: {error: execution_error}
349
- )
350
- callback_error = _deliver_stream_event(
351
- listener,
352
- terminal_event
353
- )
354
- if callback_error
355
- report_agent_event_callback_error(
356
- callback_error,
357
- event: terminal_event,
358
- invocation_id: invocation&.id,
359
- callback_error_policy: callback_error_policy
360
- )
361
- end
362
-
363
- # Listener failure never replaces an Agent execution failure.
364
- _fail_result_task(result_task, execution_error)
365
- return
366
- end
367
-
368
- terminal_event = _build_stream_terminal_event(result)
369
- callback_error = _deliver_stream_event(
370
- listener,
371
- terminal_event
372
- )
373
-
374
- unless callback_error
375
- _complete_result_task(result_task, result)
376
- return
83
+ listener = resolve_event_listener(on_event, block)
84
+ raise ArgumentError, "stream_async requires on_event: or a block" unless listener
85
+ if invocation_context
86
+ thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
377
87
  end
378
-
379
- report_agent_event_callback_error(
380
- callback_error,
381
- event: terminal_event,
382
- invocation_id: invocation&.id,
383
- callback_error_policy: callback_error_policy
88
+ approval = _approval_configuration_snapshot(on_tool_approval_required)
89
+ execution_coordinator.start(
90
+ input,
91
+ thread_id: thread_id,
92
+ config: config,
93
+ mode: :stream,
94
+ approval_policy: approval[:policy],
95
+ approval_listener: approval[:listener],
96
+ on_event: listener
384
97
  )
385
-
386
- if callback_error_policy == :fail_task
387
- wrapped = _build_stream_callback_error(
388
- event_type: terminal_event.type,
389
- callback_error: callback_error,
390
- result: result
391
- )
392
- _fail_result_task(result_task, wrapped)
393
- else
394
- _complete_result_task(result_task, result)
395
- end
396
98
  end
397
99
 
398
- def terminal_error_event_type(error)
399
- case error
400
- when Phronomy::TimeoutError
401
- :timeout
402
- when Phronomy::CancellationError
403
- :cancelled
404
- else
405
- :error
406
- end
100
+ def approve(execution_id, approval_request_id:, approved: true, config: {})
101
+ _check_scheduler_reentrancy(:approve, :approve_async)
102
+ approve_async(
103
+ execution_id,
104
+ approval_request_id: approval_request_id,
105
+ approved: approved,
106
+ config: config
107
+ ).wait_result
407
108
  end
408
109
 
409
- def normalize_terminal_error(error, invocation)
410
- return error unless error.is_a?(Phronomy::CancellationError)
411
- return error unless invocation_timeout_expired?(invocation)
412
-
413
- timeout_error = Phronomy::TimeoutError.new(error.message)
414
- timeout_error.set_backtrace(error.backtrace)
415
- timeout_error
416
- end
417
-
418
- def invocation_timeout_expired?(invocation)
419
- config = invocation&.config || {}
420
- invocation_context = config[:invocation_context]
421
- deadline = invocation_context&.deadline
422
- return true if deadline&.expired?
423
-
424
- token = config[:cancellation_token]
425
- return false unless token
426
-
427
- remaining =
428
- if token.respond_to?(:remaining_monotonic_seconds)
429
- token.remaining_monotonic_seconds
430
- end
431
- return true if remaining == 0.0
432
-
433
- wall_deadline = token.deadline if token.respond_to?(:deadline)
434
- wall_deadline && Time.now >= wall_deadline
435
- end
436
-
437
- def report_agent_event_callback_error(
438
- callback_error,
439
- event:,
440
- invocation_id:,
441
- callback_error_policy:
442
- )
443
- _report_stream_callback_error(
444
- callback_error,
445
- event: event,
446
- invocation_id: invocation_id,
447
- callback_error_policy: callback_error_policy
110
+ def approve_async(execution_id, approval_request_id:, approved: true, config: {})
111
+ execution_coordinator.resume(
112
+ execution_id,
113
+ approval_request_id: approval_request_id,
114
+ approved: approved,
115
+ config: config
448
116
  )
449
117
  end
450
118
 
451
- def _register_tool_invocation_session(
452
- event_loop,
453
- runtime,
454
- child,
455
- session
456
- )
457
- completion = Phronomy::Task.deferred(
458
- name: "tool-session:#{child.id}"
459
- )
460
- completion.on_complete do |_result, error|
461
- next unless error
119
+ private
462
120
 
463
- child.mark_framework_failed!(error)
464
- runtime.event_loop.post_to_session(
465
- Phronomy::Event.new(
466
- type: :tool_failed,
467
- target_id: child.parent_agent_invocation_id,
468
- payload: {tool_invocation_id: child.id}
469
- )
470
- )
471
- end
472
- event_loop.register(session, completion: completion)
121
+ def execution_coordinator
122
+ @execution_coordinator ||= Agent::ExecutionCoordinator.new(self)
473
123
  end
474
124
 
475
- # Resumes a suspended AgentInvocation and its pending Tool sessions.
476
- #
477
- # Parent completion handling is installed before EventLoop registration,
478
- # and the parent session is registered before child sessions so immediate
479
- # child events cannot be lost.
480
- # @api private
481
- def _start_approval_resume(
482
- result_task,
483
- invocation,
484
- approved:,
485
- config:
486
- )
487
- invocation.merge_config!(config)
488
- invocation.begin_approval_resume!(approved: approved)
489
- runtime = Phronomy::Runtime.instance
490
- event_loop = runtime.event_loop
491
- source_task = Phronomy::Task.deferred(
492
- name: "#{result_task.name}-source"
493
- )
494
- parent_session =
495
- Agent::AgentInvocationSessionBuilder.build_for_resume(
496
- agent_invocation: invocation,
497
- resume_event: :resume,
498
- resume_phase: :suspended,
499
- runtime: runtime
500
- )
501
- listener = invocation.event_listener
502
- mode = invocation.mode
503
- callback_error_policy =
504
- Phronomy.configuration.stream_callback_error_policy
505
-
506
- source_task.on_complete do |completed_invocation, error|
507
- invocation_result = completed_invocation || parent_session.context
508
- _handle_agent_completion(
509
- result_task: result_task,
510
- invocation: invocation_result,
511
- error: error,
512
- mode: mode,
513
- listener: listener,
514
- event_loop: event_loop,
515
- callback_error_policy: callback_error_policy
516
- )
517
- end
518
-
519
- event_loop.register(
520
- parent_session,
521
- completion: source_task
522
- )
523
-
524
- invocation.tool_invocations.each do |child|
525
- child_session =
526
- if child.awaiting_approval?
527
- Agent::ToolInvocationSessionBuilder.build_for_resume(
528
- tool_invocation: child,
529
- resume_event: approved ? :approve : :reject,
530
- resume_phase: :awaiting_approval,
531
- runtime: runtime
532
- )
533
- elsif !approved && child.authorized?
534
- Agent::ToolInvocationSessionBuilder.build_for_resume(
535
- tool_invocation: child,
536
- resume_event: :cancel,
537
- resume_phase: :authorized,
538
- runtime: runtime
539
- )
540
- end
541
- if child_session
542
- _register_tool_invocation_session(
543
- event_loop,
544
- runtime,
545
- child,
546
- child_session
547
- )
548
- end
125
+ def resolve_event_listener(keyword_listener, block_listener)
126
+ if keyword_listener && block_listener
127
+ raise ArgumentError, "Provide either on_event: or a block, not both"
549
128
  end
129
+ keyword_listener || block_listener
550
130
  end
551
131
  end
552
132
  end