phronomy 0.17.0 → 0.19.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -1134
  3. data/README.md +70 -1280
  4. data/benchmark/bench_regression.rb +25 -5
  5. data/docs/changelog/0.14-and-earlier.md +1137 -0
  6. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  7. data/docs/decisions/009-state-store-abstraction.md +1 -1
  8. data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
  9. data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
  10. data/docs/features.md +93 -0
  11. data/docs/getting-started.md +387 -0
  12. data/docs/migrations/0.15.md +35 -0
  13. data/docs/migrations/0.16.md +43 -0
  14. data/docs/migrations/0.19.md +148 -0
  15. data/docs/runtime-and-concurrency.md +349 -0
  16. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  17. data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
  18. data/lib/phronomy/agent/async_event_api.rb +3 -3
  19. data/lib/phronomy/agent/base.rb +116 -55
  20. data/lib/phronomy/agent/context/capability/base.rb +13 -3
  21. data/lib/phronomy/agent/context_assembler.rb +13 -3
  22. data/lib/phronomy/agent/execution_coordinator.rb +423 -252
  23. data/lib/phronomy/agent/journal_projection.rb +5 -1
  24. data/lib/phronomy/agent/shared_state.rb +2 -0
  25. data/lib/phronomy/agent/tool_executor.rb +29 -71
  26. data/lib/phronomy/agent/tool_invocation.rb +97 -47
  27. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  28. data/lib/phronomy/configuration.rb +7 -30
  29. data/lib/phronomy/diagnostics.rb +12 -41
  30. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  31. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  32. data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
  33. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  34. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  35. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  36. data/lib/phronomy/engine/event_loop.rb +170 -193
  37. data/lib/phronomy/engine/fsm_session.rb +6 -4
  38. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  39. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  40. data/lib/phronomy/engine/runtime.rb +52 -158
  41. data/lib/phronomy/engine/task.rb +136 -277
  42. data/lib/phronomy/llm_adapter/base.rb +14 -14
  43. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  44. data/lib/phronomy/llm_adapter.rb +2 -2
  45. data/lib/phronomy/metrics.rb +15 -30
  46. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  47. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  48. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  49. data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
  50. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
  51. data/lib/phronomy/persistence/in_memory.rb +113 -8
  52. data/lib/phronomy/persistence.rb +12 -3
  53. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  54. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  55. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  56. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  57. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  58. data/lib/phronomy/testing/eval/runner.rb +52 -0
  59. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  60. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  61. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  62. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  63. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  64. data/lib/phronomy/testing/eval.rb +9 -0
  65. data/lib/phronomy/testing/fake_clock.rb +6 -53
  66. data/lib/phronomy/testing.rb +2 -6
  67. data/lib/phronomy/tools/agent.rb +141 -6
  68. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  69. data/lib/phronomy/vector_store/base.rb +2 -2
  70. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  71. data/lib/phronomy/version.rb +1 -1
  72. data/lib/phronomy/workflow.rb +10 -9
  73. data/lib/phronomy/workflow_runner.rb +363 -99
  74. data/lib/phronomy.rb +10 -4
  75. data/scripts/api_snapshot.rb +4 -4
  76. metadata +25 -33
  77. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  78. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  79. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  80. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  81. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  82. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  83. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  84. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  85. data/lib/phronomy/engine/task/backend.rb +0 -80
  86. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  87. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  88. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  89. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  90. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  91. data/lib/phronomy/engine/task_group.rb +0 -193
  92. data/lib/phronomy/eval/comparison.rb +0 -47
  93. data/lib/phronomy/eval/dataset.rb +0 -45
  94. data/lib/phronomy/eval/eval_case.rb +0 -17
  95. data/lib/phronomy/eval/eval_result.rb +0 -29
  96. data/lib/phronomy/eval/metrics.rb +0 -66
  97. data/lib/phronomy/eval/runner.rb +0 -94
  98. data/lib/phronomy/eval/scorer/base.rb +0 -22
  99. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  100. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  101. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  102. data/lib/phronomy/eval/scorer.rb +0 -9
  103. data/lib/phronomy/eval.rb +0 -7
  104. data/lib/phronomy/state_store/base.rb +0 -48
  105. data/lib/phronomy/state_store/in_memory.rb +0 -62
  106. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  107. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -0,0 +1,349 @@
1
+ # Runtime and concurrency
2
+
3
+ Phronomy uses an **EventLoop / FSMSession first** architecture for framework
4
+ lifecycle coordination. `FSMSession` is the framework finite-state-machine session
5
+ used to represent explicit lifecycle state and events. A `Task` is a completion
6
+ handle, not an execution backend. Synchronous work that must stay off EventLoop is isolated in the bounded
7
+ `OffloadPool`.
8
+
9
+ For the design rationale, see Architecture Decision Record (ADR)
10
+ [ADR-010: EventLoop / FSMSession First Concurrency](decisions/010-cooperative-first-concurrency.md).
11
+ Durable-state ownership is defined by
12
+ [ADR-014: Unified Persistence and Durable-State Ownership](decisions/014-unified-persistence-durable-state.md).
13
+
14
+ ## Runtime model
15
+
16
+ ```text
17
+ Runtime
18
+ ├─ EventLoop (one control-plane operating-system Thread)
19
+ │ └─ FSMSession
20
+ │ ├─ Agent
21
+ │ ├─ Workflow
22
+ │ ├─ ToolInvocation
23
+ │ └─ MultiAgent fan-out
24
+ ├─ process-local Agent ActivationRegistry
25
+ ├─ OffloadPool (bounded operating-system Threads)
26
+ │ ├─ blocking input/output (I/O)
27
+ │ ├─ central-processing-unit (CPU)-bound synchronous work
28
+ │ └─ other long synchronous work
29
+ ├─ named OffloadPools
30
+ └─ EventLoop-driven timers
31
+
32
+ Task = completion handle
33
+ ```
34
+
35
+ The framework does not allocate one operating-system Thread per logical Agent/Workflow/Tool
36
+ lifecycle. Logical waits remain explicit states plus later EventLoop events.
37
+
38
+ ## Live state and durable state
39
+
40
+ A live Agent or Workflow owns its current logical state. `Persistence` is the
41
+ last committed durable representation and recovery source; it is not reloaded at
42
+ every semantic boundary.
43
+
44
+ For Agents, the live owner consists of the Agent instance plus its current
45
+ `AgentRoot`, hydrated Journal view, and `AgentExecutionActivation`. Mutable
46
+ Agent/Execution/Journal state is not automatically reloaded before every LLM or
47
+ Tool step. Durable writes use optimistic revision/position guardrails; an
48
+ external writer that advances the durable base causes `Persistence::ConflictError`
49
+ rather than automatic reload or merge.
50
+
51
+ For Workflows, the current `WorkflowContext` and FSMSession own the active
52
+ logical state. A durable Workflow hydrates once at invocation/resume and saves
53
+ at the halted/terminal boundary.
54
+
55
+ Content-addressed `Persistence#contents` values are immutable. Fetching a known
56
+ content reference is value materialization rather than mutable state refresh.
57
+
58
+ ## Workflow identities and durable admission
59
+
60
+ Workflow execution keeps three identities separate:
61
+
62
+ ```text
63
+ session_id
64
+ application session/correlation identity, for example a Rails session
65
+
66
+ thread_id
67
+ durable Workflow identity and Persistence#workflow_states key
68
+
69
+ fsm_session_id
70
+ one Runtime FSMSession execution identity; generated again for each
71
+ invoke/resume operation
72
+ ```
73
+
74
+ The existing application `session_id` is tracing/caller metadata and is not used
75
+ for durable Workflow ownership. EventLoop registers active FSMs by
76
+ `fsm_session_id`; durable Workflow admission is a separate owner map:
77
+
78
+ ```text
79
+ thread_id -> owner_fsm_session_id
80
+ ```
81
+
82
+ The owner is acquired before `workflow_states.load(thread_id)` and remains held
83
+ until the halted/terminal `workflow_states.save(...)` completes. Only the current
84
+ owner may release the admission. `fsm_session_id` is Runtime-only metadata and is
85
+ not stored in Workflow fields or durable snapshots.
86
+
87
+ The admission map belongs to one Runtime and is process-local. It prevents two
88
+ executions with the same durable `thread_id` from being admitted concurrently
89
+ inside that Runtime, but it is not shared across Ruby processes, containers, or
90
+ service replicas. Separate processes may therefore execute the same `thread_id`
91
+ concurrently unless the application adds distributed coordination.
92
+
93
+ `workflow_states` optimistic revisions detect stale terminal commits across those
94
+ processes. They do not prevent duplicate execution from starting and cannot undo
95
+ external side effects that both executions already performed before one save
96
+ loses the revision race. CAS is stale/double-commit detection, not a distributed
97
+ execution lock or duplicate-side-effect prevention mechanism.
98
+
99
+ ## Tool execution modes
100
+
101
+ Phronomy exposes two execution modes for capabilities:
102
+
103
+ - `:cooperative` — short EventLoop-safe work, or a specialized asynchronous Tool
104
+ that starts another Phronomy lifecycle and returns immediately.
105
+ - `:offloaded` — synchronous work that must not run to completion on EventLoop.
106
+
107
+ Phronomy does not classify application work into framework-level I/O/CPU/process
108
+ execution modes. That workload classification and capacity planning belong to the
109
+ application.
110
+
111
+ A CPU-heavy operation may therefore use `:offloaded`, but thread offload does not
112
+ remove CRuby Global VM Lock contention or physical CPU contention.
113
+
114
+ ## Logical waiting versus offload
115
+
116
+ Do not offload a logical wait merely to make it asynchronous.
117
+
118
+ Prohibited shape:
119
+
120
+ ```text
121
+ OffloadPool worker
122
+ → child_agent.invoke_async
123
+ → wait_result
124
+ ```
125
+
126
+ Correct shape:
127
+
128
+ ```text
129
+ parent FSMSession
130
+ → start child lifecycle
131
+ → return immediately
132
+ → child settles
133
+ → post parent EventLoop event
134
+ ```
135
+
136
+ This distinction prevents worker-slot starvation when many logical lifecycles are
137
+ waiting at the same time.
138
+
139
+ ## Persistence I/O boundary
140
+
141
+ `Persistence` repositories expose synchronous operations. Framework lifecycle
142
+ code must not perform potentially blocking durable reads/writes on EventLoop.
143
+ Agent preparation/commit and Workflow hydrate/save operations are submitted to
144
+ `OffloadPool`; completion continues through completion callbacks or explicit
145
+ EventLoop events.
146
+
147
+ A durable barrier may pause one logical lifecycle without blocking EventLoop.
148
+ The next Agent provider call does not start until the corresponding Manifest and
149
+ logical execution snapshot commit succeeds. A persistence failure or optimistic
150
+ conflict fails that step rather than continuing with stale state.
151
+
152
+ Approval wait is not a hydration boundary. The same live Agent instance,
153
+ Activation, and AgentInvocation remain the owner and are resumed after approval.
154
+ Approval itself remains an Agent-instance operation. An application that only has
155
+ an `execution_id` first resolves the current process's owner with
156
+ `Phronomy::Agent::Base.live_for_execution(execution_id)` or the expected concrete
157
+ Agent class, then calls `agent.approve(...)` or `agent.approve_async(...)`.
158
+ `live_for_execution` consults the Runtime-local ActivationRegistry and does not
159
+ load a replacement Agent from Persistence.
160
+
161
+ ## Sync versus async application APIs
162
+
163
+ | Calling context | Recommended approach |
164
+ |---|---|
165
+ | Top-level application code | `agent.invoke(...)` when blocking the caller is acceptable |
166
+ | Top-level explicit async | `task = agent.invoke_async(...)`; optionally `task.wait_result` outside EventLoop |
167
+ | Workflow entry/transition action | Start async work and continue through `Workflow#signal` |
168
+ | EventLoop callback | Never block waiting for a Task that requires EventLoop progress |
169
+ | Top-level streaming | `agent.stream(...)` |
170
+ | Non-blocking streaming | `agent.stream_async(...)` |
171
+ | Approval from EventLoop callback | Resolve with `live_for_execution`, call `agent.approve_async(...)`, and return immediately |
172
+
173
+ Blocking synchronous APIs reject EventLoop re-entry with
174
+ `Phronomy::EventLoopReentrancyError` when waiting would stall the same EventLoop
175
+ needed for progress. `live_for_execution` itself only performs a Runtime-local
176
+ registry lookup and does not wait for Task progress.
177
+
178
+ ## Task
179
+
180
+ `Phronomy::Task` is thread-free. It represents one terminal result:
181
+
182
+ - completed value,
183
+ - failure,
184
+ - cancellation.
185
+
186
+ `Task#wait_result(timeout:)` is a bridge for external synchronous callers. It is
187
+ not the framework continuation mechanism.
188
+
189
+ `Task#map` is application-level composition. A transformation exception settles
190
+ the mapped Task as failed. This is different from independent notification
191
+ callbacks, described below.
192
+
193
+ ## OffloadPool
194
+
195
+ `OffloadPool` is a bounded worker pool for synchronous work that must not execute
196
+ on EventLoop.
197
+
198
+ Its guarantees include:
199
+
200
+ - bounded worker count,
201
+ - bounded queue depth,
202
+ - queue backpressure,
203
+ - operation-wide submit timeout/cancellation settlement,
204
+ - abandoned-worker accounting,
205
+ - runtime metrics,
206
+ - shutdown/drain behavior.
207
+
208
+ It does not guarantee CPU/I/O fairness or CPU isolation. Applications that need
209
+ resource isolation can create named Runtime pools.
210
+
211
+ ### EventLoop queue admission
212
+
213
+ Framework-owned EventLoop-origin submissions must not wait for a free worker
214
+ queue slot. They use non-blocking admission (`on_full: :raise`) and route
215
+ `BackpressureError` through the ordinary FSM/completion path.
216
+
217
+ External management threads may choose a blocking admission policy when blocking
218
+ the caller is acceptable.
219
+
220
+ ## PendingOperation and blocking_wait
221
+
222
+ `OffloadPool#submit` returns a private `PendingOperation` immediately after queue
223
+ admission.
224
+
225
+ `PendingOperation#blocking_wait(timeout:)` is intentionally a **low-level
226
+ synchronous bridge** for non-EventLoop callers such as tests and diagnostics.
227
+ The timeout belongs only to that waiter:
228
+
229
+ - it raises `TimeoutError` to that calling thread,
230
+ - it does not settle the PendingOperation,
231
+ - it does not cancel the submitted operation,
232
+ - it does not mark the operation abandoned.
233
+
234
+ There is no waiter-local `cancellation_token:` argument. Operation-wide
235
+ cancellation belongs exclusively to `OffloadPool#submit(cancellation_token:)`.
236
+
237
+ ## Submit timeout and cancellation
238
+
239
+ Submit-time timeout and submit cancellation settle the caller-facing operation.
240
+ They do **not** asynchronously interrupt an already-running synchronous worker.
241
+
242
+ ### Before worker start
243
+
244
+ If timeout/cancellation wins before execution starts:
245
+
246
+ - the PendingOperation settles,
247
+ - the submitted block does not run,
248
+ - the operation is not counted as abandoned.
249
+
250
+ ### After worker start
251
+
252
+ If timeout/cancellation wins after execution starts:
253
+
254
+ - the PendingOperation settles immediately,
255
+ - the operation is marked abandoned,
256
+ - the worker is allowed to continue until its synchronous call returns,
257
+ - the eventual worker result is discarded.
258
+
259
+ Phronomy does not use `Thread#raise` to inject an exception into the worker.
260
+ Application/library code that needs hard or transport-level deadlines should use
261
+ its native timeout or, in the future, an appropriate process-isolation mechanism.
262
+
263
+ ### CancellationToken deadlines
264
+
265
+ `CancellationToken.timeout_after(seconds)` uses a monotonic deadline.
266
+ `cancelled?` becomes true after that deadline, but the token itself does not own a
267
+ Thread.
268
+
269
+ Components requiring callback delivery for a monotonic deadline must promote the
270
+ deadline to explicit `cancel!` through the Runtime timer queue. OffloadPool does
271
+ this for its submit cancellation token.
272
+
273
+ `CancellationScope#deadline_in` is appropriate when the application needs a
274
+ Runtime-timer-backed cancellation scope whose `on_cancel` subscribers are fired
275
+ on expiry.
276
+
277
+ ## Independent notification callbacks
278
+
279
+ Independent notification fan-out is fault-isolated.
280
+
281
+ The rule applies to:
282
+
283
+ - `CancellationToken#on_cancel`,
284
+ - `Task#on_complete`,
285
+ - `PendingOperation#on_complete`,
286
+ - EventLoop timer callbacks.
287
+
288
+ A `StandardError` from one independent subscriber is logged and does not suppress
289
+ later subscribers.
290
+
291
+ This is deliberately different from a continuation/transform such as
292
+ `Task#map`: a transform exception is the outcome of the derived operation and is
293
+ therefore propagated into that derived Task.
294
+
295
+ Callback execution thread is not guaranteed for low-level completion handles.
296
+ Callbacks must therefore be thread-safe and should complete quickly. Framework
297
+ lifecycle code normally turns completion into an explicit EventLoop event rather
298
+ than mutating unrelated logical state from a worker thread.
299
+
300
+ ## Abandoned-worker metrics
301
+
302
+ Two metrics answer different operational questions:
303
+
304
+ - `offload_pool_abandoned_total` — cumulative count of operations that became
305
+ abandoned after worker execution had started.
306
+ - `offload_pool_abandoned_active` — current number of abandoned operations whose
307
+ synchronous workers still occupy pool capacity.
308
+
309
+ Example:
310
+
311
+ ```text
312
+ offload_pool_size = 10
313
+ offload_pool_active = 10
314
+ offload_pool_abandoned_active = 8
315
+ offload_pool_abandoned_total = 523
316
+ offload_pool_queue_length = 40
317
+ ```
318
+
319
+ This means 10 workers are currently executing, 8 of them are doing work whose
320
+ caller-facing result has already been abandoned, 523 abandonment events have
321
+ occurred since process start, and 40 operations are queued.
322
+
323
+ `Phronomy::Diagnostics.dump` exposes the same distinction for point-in-time
324
+ troubleshooting.
325
+
326
+ ## EventLoop metrics
327
+
328
+ `Phronomy::Metrics.snapshot` also reports EventLoop queue depth and lag values.
329
+ Use these to distinguish worker saturation from EventLoop backlog/latency.
330
+
331
+ ## Shutdown
332
+
333
+ `Runtime#shutdown` is terminal for that Runtime. It drains/terminates the
334
+ Runtime-owned EventLoop, then closes pools and timers according to the Runtime
335
+ shutdown contract.
336
+
337
+ Workflow durable admission participates in EventLoop idleness: a Workflow whose
338
+ FSMSession has ended but whose durable save is still in flight remains owned until
339
+ that save completes and owner-aware admission is released.
340
+
341
+ `Phronomy.reset_runtime!` exists primarily for test isolation and performs a real
342
+ Runtime shutdown before resetting configuration.
343
+
344
+ ## Further design records
345
+
346
+ The `docs/decisions/` directory contains the historical and current Architecture
347
+ Decision Records (ADRs). When an older ADR is superseded, use the superseding
348
+ section/current ADR as the active design contract and keep the earlier document
349
+ as historical rationale.
@@ -19,18 +19,25 @@ class ImportContext
19
19
  end
20
20
  end
21
21
 
22
+ # Example application service for synchronous work that must stay off EventLoop.
23
+ # The worker Thread belongs to Phronomy's bounded OffloadPool; the Workflow
24
+ # itself never blocks EventLoop waiting for the operation.
25
+ def start_import_async
26
+ Phronomy::Runtime.instance.offload.submit(on_full: :raise) do
27
+ # Replace with blocking file/DB/network work or another long synchronous call.
28
+ 100
29
+ end
30
+ end
31
+
22
32
  workflow = nil
23
33
 
24
34
  workflow = Phronomy::Workflow.define(ImportContext) do
25
35
  initial :importing
26
36
 
27
37
  state :importing, action: ->(context) {
28
- task = Phronomy::Runtime.instance.spawn do
29
- # Replace with application-owned asynchronous work.
30
- 100
31
- end
38
+ operation = start_import_async
32
39
 
33
- task.on_complete do |record_count, error|
40
+ operation.on_complete do |record_count, error|
34
41
  workflow.signal(
35
42
  thread_id: context.thread_id,
36
43
  event: error ? :import_failed : :import_completed,
@@ -41,7 +48,8 @@ workflow = Phronomy::Workflow.define(ImportContext) do
41
48
  )
42
49
  end
43
50
 
44
- # Do not return task. The state is active after this synchronous entry ends.
51
+ # Do not return the completion handle. The state remains active after this
52
+ # synchronous action returns, and later completion arrives as an FSM event.
45
53
  context
46
54
  }
47
55
 
@@ -4,7 +4,7 @@ module Phronomy
4
4
  module Agent
5
5
  # Builds FSMSession instances for AgentInvocation objects.
6
6
  #
7
- # Blocking/provider work returns through explicit Agent-internal events.
7
+ # Offloaded/provider work returns through explicit Agent-internal events.
8
8
  # Entry actions start operations and return synchronously.
9
9
  # Every LLM Call is prepared from a canonical Manifest and RuntimeProjection.
10
10
  #
@@ -272,7 +272,7 @@ module Phronomy
272
272
  def self.prepare_and_start_llm_call(agent, runtime, invocation, streaming:)
273
273
  activation = invocation.config.fetch(:phronomy_activation)
274
274
  if invocation.user_message_sent
275
- preparation = runtime.blocking_io.submit do
275
+ preparation = runtime.offload.submit(on_full: :raise) do
276
276
  activation.coordinator.prepare_next_llm_call(activation)
277
277
  end
278
278
  preparation.on_complete do |projection, error|
@@ -9,7 +9,7 @@ module Phronomy
9
9
  if invocation_context
10
10
  thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
11
11
  end
12
- _check_scheduler_reentrancy(:invoke, :invoke_async)
12
+ _check_event_loop_reentrancy(:invoke, :invoke_async)
13
13
  trace("agent.invoke", input: input, **_build_caller_meta(config)) do |_span|
14
14
  result = invoke_async(
15
15
  input,
@@ -58,7 +58,7 @@ module Phronomy
58
58
  if invocation_context
59
59
  thread_id, config = _apply_invocation_context(thread_id, config, invocation_context)
60
60
  end
61
- _check_scheduler_reentrancy(:stream, :stream_async)
61
+ _check_event_loop_reentrancy(:stream, :stream_async)
62
62
  trace("agent.stream", input: input, **_build_caller_meta(config)) do |_span|
63
63
  result = stream_async(
64
64
  input,
@@ -98,7 +98,7 @@ module Phronomy
98
98
  end
99
99
 
100
100
  def approve(execution_id, approval_request_id:, approved: true, config: {})
101
- _check_scheduler_reentrancy(:approve, :approve_async)
101
+ _check_event_loop_reentrancy(:approve, :approve_async)
102
102
  approve_async(
103
103
  execution_id,
104
104
  approval_request_id: approval_request_id,