phronomy 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/README.md +266 -38
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +58 -53
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- data/lib/phronomy/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "state_machines"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
# Builds an FSMSession for one ToolInvocation.
|
|
8
|
+
#
|
|
9
|
+
# Authorization and execution Tasks are observed by ToolInvocation-specific
|
|
10
|
+
# callbacks that post explicit FSM events. The generated phase machine does
|
|
11
|
+
# not await Tasks.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class ToolInvocationSessionBuilder
|
|
15
|
+
AUTO_STATE_SET = {
|
|
16
|
+
idle: true,
|
|
17
|
+
validating: true,
|
|
18
|
+
queued: true
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DECLARED_STATES = %i[
|
|
22
|
+
idle validating authorizing awaiting_approval authorized queued running
|
|
23
|
+
completed failed rejected cancelled
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
WAIT_STATES = %i[awaiting_approval authorized].freeze
|
|
27
|
+
|
|
28
|
+
EXTERNAL_EVENTS = {
|
|
29
|
+
authorization_completed: [
|
|
30
|
+
{from: :authorizing, to: :cancelled, guard: ->(ctx) {
|
|
31
|
+
ctx.cancelled?
|
|
32
|
+
}},
|
|
33
|
+
{from: :authorizing, to: :failed, guard: ->(ctx) {
|
|
34
|
+
ctx.failed?
|
|
35
|
+
}},
|
|
36
|
+
{from: :authorizing, to: :rejected, guard: ->(ctx) {
|
|
37
|
+
ctx.rejected?
|
|
38
|
+
}},
|
|
39
|
+
{from: :authorizing, to: :awaiting_approval, guard: ->(ctx) {
|
|
40
|
+
ctx.awaiting_approval?
|
|
41
|
+
}},
|
|
42
|
+
{from: :authorizing, to: :authorized, guard: ->(ctx) {
|
|
43
|
+
ctx.authorized?
|
|
44
|
+
}}
|
|
45
|
+
],
|
|
46
|
+
execution_completed: [
|
|
47
|
+
{from: :running, to: :cancelled, guard: ->(ctx) {
|
|
48
|
+
ctx.cancelled?
|
|
49
|
+
}},
|
|
50
|
+
{from: :running, to: :failed, guard: ->(ctx) {
|
|
51
|
+
ctx.failed?
|
|
52
|
+
}},
|
|
53
|
+
{from: :running, to: :completed, guard: ->(ctx) {
|
|
54
|
+
ctx.execution_completed?
|
|
55
|
+
}}
|
|
56
|
+
],
|
|
57
|
+
approve: [
|
|
58
|
+
{from: :awaiting_approval, to: :authorized, guard: nil}
|
|
59
|
+
],
|
|
60
|
+
reject: [
|
|
61
|
+
{from: :awaiting_approval, to: :rejected, guard: nil}
|
|
62
|
+
],
|
|
63
|
+
dispatch: [
|
|
64
|
+
{from: :authorized, to: :queued, guard: nil}
|
|
65
|
+
],
|
|
66
|
+
cancel: [
|
|
67
|
+
{from: :awaiting_approval, to: :cancelled, guard: nil},
|
|
68
|
+
{from: :authorized, to: :cancelled, guard: nil},
|
|
69
|
+
{from: :queued, to: :cancelled, guard: nil},
|
|
70
|
+
{from: :running, to: :cancelled, guard: nil}
|
|
71
|
+
]
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
def self.build(
|
|
75
|
+
tool_invocation:,
|
|
76
|
+
runtime: Phronomy::Runtime.instance
|
|
77
|
+
)
|
|
78
|
+
build_session(
|
|
79
|
+
tool_invocation: tool_invocation,
|
|
80
|
+
runtime: runtime
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.build_for_resume(
|
|
85
|
+
tool_invocation:,
|
|
86
|
+
resume_event:,
|
|
87
|
+
resume_phase:,
|
|
88
|
+
runtime: Phronomy::Runtime.instance
|
|
89
|
+
)
|
|
90
|
+
build_session(
|
|
91
|
+
tool_invocation: tool_invocation,
|
|
92
|
+
runtime: runtime,
|
|
93
|
+
resume_event: resume_event,
|
|
94
|
+
resume_phase: resume_phase
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.build_session(
|
|
99
|
+
tool_invocation:,
|
|
100
|
+
runtime:,
|
|
101
|
+
resume_event: nil,
|
|
102
|
+
resume_phase: nil
|
|
103
|
+
)
|
|
104
|
+
actions = build_entry_actions(runtime)
|
|
105
|
+
phase_machine = build_phase_machine(actions)
|
|
106
|
+
|
|
107
|
+
Phronomy::FSMSession.new(
|
|
108
|
+
id: tool_invocation.id,
|
|
109
|
+
context: tool_invocation,
|
|
110
|
+
entry_point: :idle,
|
|
111
|
+
phase_machine_class: phase_machine,
|
|
112
|
+
entry_actions: {},
|
|
113
|
+
auto_state_set: AUTO_STATE_SET,
|
|
114
|
+
declared_states: DECLARED_STATES,
|
|
115
|
+
wait_state_names: WAIT_STATES,
|
|
116
|
+
external_events: EXTERNAL_EVENTS,
|
|
117
|
+
recursion_limit: 20,
|
|
118
|
+
event_loop: runtime.event_loop,
|
|
119
|
+
resume_event: resume_event,
|
|
120
|
+
resume_phase: resume_phase
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
private_class_method :build_session
|
|
124
|
+
|
|
125
|
+
def self.build_entry_actions(runtime)
|
|
126
|
+
{
|
|
127
|
+
validating: [method(:validating_action)],
|
|
128
|
+
authorizing: [
|
|
129
|
+
method(:authorizing_action).curry.call(runtime)
|
|
130
|
+
],
|
|
131
|
+
awaiting_approval: [
|
|
132
|
+
method(:awaiting_approval_action).curry.call(runtime)
|
|
133
|
+
],
|
|
134
|
+
authorized: [
|
|
135
|
+
method(:authorized_action).curry.call(runtime)
|
|
136
|
+
],
|
|
137
|
+
queued: [method(:queued_action)],
|
|
138
|
+
running: [
|
|
139
|
+
method(:running_action).curry.call(runtime)
|
|
140
|
+
],
|
|
141
|
+
completed: [
|
|
142
|
+
method(:completed_action).curry.call(runtime)
|
|
143
|
+
],
|
|
144
|
+
failed: [
|
|
145
|
+
method(:failed_action).curry.call(runtime)
|
|
146
|
+
],
|
|
147
|
+
rejected: [
|
|
148
|
+
method(:rejected_action).curry.call(runtime)
|
|
149
|
+
],
|
|
150
|
+
cancelled: [
|
|
151
|
+
method(:cancelled_action).curry.call(runtime)
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
private_class_method :build_entry_actions
|
|
156
|
+
|
|
157
|
+
def self.build_phase_machine(entry_actions)
|
|
158
|
+
callbacks = entry_actions
|
|
159
|
+
callback_builder = method(:build_entry_callback)
|
|
160
|
+
|
|
161
|
+
Class.new do
|
|
162
|
+
attr_accessor :context, :current_event
|
|
163
|
+
|
|
164
|
+
state_machine :phase, initial: :idle do
|
|
165
|
+
state :idle
|
|
166
|
+
state :validating
|
|
167
|
+
state :authorizing
|
|
168
|
+
state :awaiting_approval
|
|
169
|
+
state :authorized
|
|
170
|
+
state :queued
|
|
171
|
+
state :running
|
|
172
|
+
state :completed
|
|
173
|
+
state :failed
|
|
174
|
+
state :rejected
|
|
175
|
+
state :cancelled
|
|
176
|
+
|
|
177
|
+
event :state_completed do
|
|
178
|
+
transition idle: :validating
|
|
179
|
+
|
|
180
|
+
transition validating: :failed,
|
|
181
|
+
if: ->(machine) { machine.context&.failed? }
|
|
182
|
+
transition validating: :completed,
|
|
183
|
+
if: ->(machine) {
|
|
184
|
+
machine.context&.validation_completed?
|
|
185
|
+
}
|
|
186
|
+
transition validating: :authorizing,
|
|
187
|
+
if: ->(machine) {
|
|
188
|
+
machine.context&.validation_passed?
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
transition queued: :running
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
event :authorization_completed do
|
|
195
|
+
transition authorizing: :cancelled,
|
|
196
|
+
if: ->(machine) { machine.context&.cancelled? }
|
|
197
|
+
transition authorizing: :failed,
|
|
198
|
+
if: ->(machine) { machine.context&.failed? }
|
|
199
|
+
transition authorizing: :rejected,
|
|
200
|
+
if: ->(machine) { machine.context&.rejected? }
|
|
201
|
+
transition authorizing: :awaiting_approval,
|
|
202
|
+
if: ->(machine) {
|
|
203
|
+
machine.context&.awaiting_approval?
|
|
204
|
+
}
|
|
205
|
+
transition authorizing: :authorized,
|
|
206
|
+
if: ->(machine) { machine.context&.authorized? }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
event :execution_completed do
|
|
210
|
+
transition running: :cancelled,
|
|
211
|
+
if: ->(machine) { machine.context&.cancelled? }
|
|
212
|
+
transition running: :failed,
|
|
213
|
+
if: ->(machine) { machine.context&.failed? }
|
|
214
|
+
transition running: :completed,
|
|
215
|
+
if: ->(machine) {
|
|
216
|
+
machine.context&.execution_completed?
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
event :approve do
|
|
221
|
+
transition awaiting_approval: :authorized
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
event :reject do
|
|
225
|
+
transition awaiting_approval: :rejected
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
event :dispatch do
|
|
229
|
+
transition authorized: :queued
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
event :cancel do
|
|
233
|
+
transition awaiting_approval: :cancelled
|
|
234
|
+
transition authorized: :cancelled
|
|
235
|
+
transition queued: :cancelled
|
|
236
|
+
transition running: :cancelled
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
callbacks.each do |state_name, callables|
|
|
240
|
+
callables.each do |callable|
|
|
241
|
+
after_transition(
|
|
242
|
+
to: state_name,
|
|
243
|
+
do: callback_builder.call(callable, state_name)
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
private_class_method :build_phase_machine
|
|
251
|
+
|
|
252
|
+
def self.build_entry_callback(callable, state_name)
|
|
253
|
+
->(machine) {
|
|
254
|
+
result = callable.call(machine.context)
|
|
255
|
+
if result.is_a?(Phronomy::Task)
|
|
256
|
+
raise Phronomy::InvalidAsyncEntryActionError,
|
|
257
|
+
"Tool entry action for #{state_name.inspect} returned Phronomy::Task"
|
|
258
|
+
end
|
|
259
|
+
machine.context = result if result.respond_to?(:set_graph_metadata)
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
private_class_method :build_entry_callback
|
|
263
|
+
|
|
264
|
+
def self.validating_action(invocation)
|
|
265
|
+
invocation.validate!
|
|
266
|
+
end
|
|
267
|
+
private_class_method :validating_action
|
|
268
|
+
|
|
269
|
+
def self.authorizing_action(runtime, invocation)
|
|
270
|
+
task = invocation.authorization_task(runtime: runtime)
|
|
271
|
+
observe_task(
|
|
272
|
+
runtime,
|
|
273
|
+
invocation,
|
|
274
|
+
task,
|
|
275
|
+
event_type: :authorization_completed
|
|
276
|
+
)
|
|
277
|
+
invocation
|
|
278
|
+
end
|
|
279
|
+
private_class_method :authorizing_action
|
|
280
|
+
|
|
281
|
+
def self.awaiting_approval_action(runtime, invocation)
|
|
282
|
+
invocation.mark_awaiting_approval!
|
|
283
|
+
notify_parent(runtime, invocation, :tool_approval_required)
|
|
284
|
+
invocation
|
|
285
|
+
end
|
|
286
|
+
private_class_method :awaiting_approval_action
|
|
287
|
+
|
|
288
|
+
def self.authorized_action(runtime, invocation)
|
|
289
|
+
invocation.mark_authorized!
|
|
290
|
+
notify_parent(runtime, invocation, :tool_authorized)
|
|
291
|
+
invocation
|
|
292
|
+
end
|
|
293
|
+
private_class_method :authorized_action
|
|
294
|
+
|
|
295
|
+
def self.queued_action(invocation)
|
|
296
|
+
invocation.mark_queued!
|
|
297
|
+
end
|
|
298
|
+
private_class_method :queued_action
|
|
299
|
+
|
|
300
|
+
def self.running_action(runtime, invocation)
|
|
301
|
+
# execution_task checks dispatchable? which requires :queued status;
|
|
302
|
+
# mark_running! is deferred until after the task is started.
|
|
303
|
+
task = invocation.execution_task(runtime: runtime)
|
|
304
|
+
invocation.mark_running!
|
|
305
|
+
observe_task(
|
|
306
|
+
runtime,
|
|
307
|
+
invocation,
|
|
308
|
+
task,
|
|
309
|
+
event_type: :execution_completed
|
|
310
|
+
)
|
|
311
|
+
invocation
|
|
312
|
+
end
|
|
313
|
+
private_class_method :running_action
|
|
314
|
+
|
|
315
|
+
def self.observe_task(
|
|
316
|
+
runtime,
|
|
317
|
+
invocation,
|
|
318
|
+
task,
|
|
319
|
+
event_type:
|
|
320
|
+
)
|
|
321
|
+
task.on_complete do |outcome, error|
|
|
322
|
+
payload = error || outcome
|
|
323
|
+
accepted = runtime.event_loop.post_to_session(
|
|
324
|
+
Phronomy::Event.new(
|
|
325
|
+
type: event_type,
|
|
326
|
+
target_id: invocation.id,
|
|
327
|
+
payload: payload
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
next if accepted
|
|
331
|
+
|
|
332
|
+
Phronomy.configuration.logger&.warn(
|
|
333
|
+
"[Phronomy] Dropped #{event_type.inspect} for " \
|
|
334
|
+
"ToolInvocation #{invocation.id}"
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
private_class_method :observe_task
|
|
339
|
+
|
|
340
|
+
def self.completed_action(runtime, invocation)
|
|
341
|
+
notify_parent(runtime, invocation, :tool_completed)
|
|
342
|
+
invocation
|
|
343
|
+
end
|
|
344
|
+
private_class_method :completed_action
|
|
345
|
+
|
|
346
|
+
def self.failed_action(runtime, invocation)
|
|
347
|
+
notify_parent(runtime, invocation, :tool_failed)
|
|
348
|
+
invocation
|
|
349
|
+
end
|
|
350
|
+
private_class_method :failed_action
|
|
351
|
+
|
|
352
|
+
def self.rejected_action(runtime, invocation)
|
|
353
|
+
invocation.mark_rejected!
|
|
354
|
+
notify_parent(runtime, invocation, :tool_rejected)
|
|
355
|
+
invocation
|
|
356
|
+
end
|
|
357
|
+
private_class_method :rejected_action
|
|
358
|
+
|
|
359
|
+
def self.cancelled_action(runtime, invocation)
|
|
360
|
+
invocation.mark_cancelled!
|
|
361
|
+
notify_parent(runtime, invocation, :tool_cancelled)
|
|
362
|
+
invocation
|
|
363
|
+
end
|
|
364
|
+
private_class_method :cancelled_action
|
|
365
|
+
|
|
366
|
+
def self.notify_parent(runtime, invocation, event_type)
|
|
367
|
+
runtime.event_loop.post_to_session(
|
|
368
|
+
Phronomy::Event.new(
|
|
369
|
+
type: event_type,
|
|
370
|
+
target_id: invocation.parent_agent_invocation_id,
|
|
371
|
+
payload: {tool_invocation_id: invocation.id}
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
private_class_method :notify_parent
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
data/lib/phronomy/agent.rb
CHANGED
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module Agent
|
|
5
|
-
#
|
|
5
|
+
# Immutable event emitted by Agent async APIs.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# :
|
|
13
|
-
#
|
|
14
|
-
# :
|
|
7
|
+
# invoke_async and stream_async share lifecycle and Tool events. Streaming
|
|
8
|
+
# additionally emits :token events.
|
|
9
|
+
#
|
|
10
|
+
# Common event types:
|
|
11
|
+
# :tool_call
|
|
12
|
+
# :tool_result
|
|
13
|
+
# :approval_required
|
|
14
|
+
# :done
|
|
15
|
+
# :error
|
|
16
|
+
# :timeout
|
|
17
|
+
# :cancelled
|
|
18
|
+
#
|
|
19
|
+
# Streaming-only event type:
|
|
20
|
+
# :token
|
|
15
21
|
StreamEvent = Data.define(:type, :payload)
|
|
16
22
|
end
|
|
17
23
|
end
|
|
24
|
+
|
|
25
|
+
require_relative "agent/async_event_api"
|
|
26
|
+
|
|
27
|
+
unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
|
|
28
|
+
Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
|
|
29
|
+
end
|
|
@@ -10,6 +10,9 @@ module Phronomy
|
|
|
10
10
|
# config.recursion_limit = 50
|
|
11
11
|
# end
|
|
12
12
|
class Configuration
|
|
13
|
+
STREAM_CALLBACK_ERROR_POLICIES = %i[report fail_task].freeze
|
|
14
|
+
private_constant :STREAM_CALLBACK_ERROR_POLICIES
|
|
15
|
+
|
|
13
16
|
# Default LLM model name (nil delegates to RubyLLM default)
|
|
14
17
|
attr_accessor :default_model
|
|
15
18
|
|
|
@@ -86,20 +89,6 @@ module Phronomy
|
|
|
86
89
|
# Phronomy.configure { |c| c.llm_adapter = MyAsyncLLMAdapter.new }
|
|
87
90
|
attr_accessor :llm_adapter
|
|
88
91
|
|
|
89
|
-
# Default backpressure strategy for {BlockingAdapterPool#submit} when the
|
|
90
|
-
# queue is full. One of +:wait+ (block until a slot is available),
|
|
91
|
-
# +:raise+ (raise {Phronomy::BackpressureError}), or +:timeout+ (raise
|
|
92
|
-
# {Phronomy::TimeoutError} after +backpressure_timeout+ seconds).
|
|
93
|
-
# @return [:wait, :raise, :timeout]
|
|
94
|
-
attr_accessor :backpressure
|
|
95
|
-
|
|
96
|
-
# Seconds to wait before raising {Phronomy::TimeoutError} when
|
|
97
|
-
# +backpressure+ is +:timeout+.
|
|
98
|
-
# @return [Numeric, nil]
|
|
99
|
-
attr_accessor :backpressure_timeout
|
|
100
|
-
|
|
101
|
-
# Warn when an event spends longer than this many seconds waiting in the
|
|
102
|
-
# EventLoop queue before being dispatched (starvation detection).
|
|
103
92
|
# Set to +nil+ to disable the warning.
|
|
104
93
|
# @return [Numeric, nil]
|
|
105
94
|
attr_accessor :event_loop_starvation_threshold_seconds
|
|
@@ -120,37 +109,45 @@ module Phronomy
|
|
|
120
109
|
# @return [Float, nil]
|
|
121
110
|
attr_accessor :blocking_detect_threshold_ms
|
|
122
111
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
112
|
+
# Determines how an unhandled Application exception from a terminal stream
|
|
113
|
+
# callback affects the Task returned by Agent#stream_async or
|
|
114
|
+
# Agent#approve_async.
|
|
115
|
+
#
|
|
116
|
+
# +:report+ logs the callback failure and preserves the Agent result.
|
|
117
|
+
# +:fail_task+ logs the callback failure and fails the current Task with
|
|
118
|
+
# {Phronomy::StreamCallbackError}. Neither policy terminates EventLoop.
|
|
119
|
+
#
|
|
120
|
+
# Default: +:report+.
|
|
121
|
+
# @return [:report, :fail_task]
|
|
122
|
+
attr_reader :stream_callback_error_policy
|
|
123
|
+
|
|
124
|
+
# Number of OS worker threads in the default {BlockingAdapterPool}.
|
|
125
|
+
# All LLM calls, MCP tool calls, and other blocking I/O share this pool.
|
|
126
|
+
# Increase for higher LLM/tool throughput; decrease to limit
|
|
127
|
+
# concurrency (e.g. to stay within a provider's rate limit).
|
|
128
|
+
# Default: 10.
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
attr_accessor :blocking_io_pool_size
|
|
131
|
+
|
|
132
|
+
# Maximum number of operations that may wait in the {BlockingAdapterPool}
|
|
133
|
+
# queue before {Phronomy::BackpressureError} is raised (on_full: :raise) or
|
|
134
|
+
# the caller blocks (on_full: :wait, the default). Default: 100.
|
|
135
|
+
# @return [Integer]
|
|
136
|
+
attr_accessor :blocking_io_queue_size
|
|
137
|
+
|
|
138
|
+
# Worker count for Tool authorization evaluation. The named pool is owned
|
|
139
|
+
# by Runtime#pool(:authorization) and shares PoolRegistry lifecycle.
|
|
140
|
+
# @return [Integer]
|
|
141
|
+
attr_accessor :authorization_pool_size
|
|
142
|
+
|
|
143
|
+
# Maximum queued Tool authorization evaluations.
|
|
144
|
+
# @return [Integer]
|
|
145
|
+
attr_accessor :authorization_queue_size
|
|
146
|
+
|
|
147
|
+
# Operation-wide deadline for approval_facts, requires_approval callables,
|
|
148
|
+
# and Agent#tool_approval_policy. Timeout fails closed to Human approval.
|
|
149
|
+
# @return [Numeric]
|
|
150
|
+
attr_accessor :authorization_timeout
|
|
154
151
|
|
|
155
152
|
# Scheduler starvation threshold (milliseconds).
|
|
156
153
|
# When a task waits more than this many milliseconds after calling
|
|
@@ -188,6 +185,16 @@ module Phronomy
|
|
|
188
185
|
# @return [Boolean]
|
|
189
186
|
attr_accessor :strict_runtime_guards
|
|
190
187
|
|
|
188
|
+
def stream_callback_error_policy=(value)
|
|
189
|
+
unless STREAM_CALLBACK_ERROR_POLICIES.include?(value)
|
|
190
|
+
allowed = STREAM_CALLBACK_ERROR_POLICIES.map(&:inspect).join(", ")
|
|
191
|
+
raise Phronomy::ConfigurationError,
|
|
192
|
+
"stream_callback_error_policy must be one of: #{allowed}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
@stream_callback_error_policy = value
|
|
196
|
+
end
|
|
197
|
+
|
|
191
198
|
def initialize
|
|
192
199
|
@recursion_limit = 25
|
|
193
200
|
@tracer = Phronomy::Tracing::NullTracer.new
|
|
@@ -195,18 +202,16 @@ module Phronomy
|
|
|
195
202
|
@parallel_tool_execution = false
|
|
196
203
|
@event_loop_stop_grace_seconds = 5
|
|
197
204
|
@llm_adapter = Phronomy::LLMAdapter::RubyLLM.new
|
|
198
|
-
@backpressure = :wait
|
|
199
|
-
@backpressure_timeout = nil
|
|
200
205
|
@event_loop_starvation_threshold_seconds = nil
|
|
201
206
|
@event_loop_dispatch_threshold_seconds = nil
|
|
202
207
|
@scheduler_debug = false
|
|
203
208
|
@blocking_detect_threshold_ms = nil
|
|
204
|
-
@
|
|
205
|
-
@
|
|
206
|
-
@
|
|
207
|
-
@
|
|
208
|
-
@
|
|
209
|
-
@
|
|
209
|
+
@stream_callback_error_policy = :report
|
|
210
|
+
@blocking_io_pool_size = 10
|
|
211
|
+
@blocking_io_queue_size = 100
|
|
212
|
+
@authorization_pool_size = 4
|
|
213
|
+
@authorization_queue_size = 100
|
|
214
|
+
@authorization_timeout = 5
|
|
210
215
|
@starvation_threshold_ms = 50
|
|
211
216
|
@runtime_backend = :thread
|
|
212
217
|
@strict_runtime_guards = false
|
data/lib/phronomy/diagnostics.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Phronomy
|
|
|
52
52
|
# @return [void]
|
|
53
53
|
# @api private
|
|
54
54
|
def self.assert_not_in_event_loop!
|
|
55
|
-
return unless Phronomy::
|
|
55
|
+
return unless Phronomy::Runtime.in_event_loop_context?
|
|
56
56
|
|
|
57
57
|
raise Phronomy::SchedulerReentrancyError,
|
|
58
58
|
"Blocking invoke called from inside an EventLoop action. " \
|