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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +122 -1134
- data/README.md +70 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +93 -0
- data/docs/getting-started.md +387 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +349 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +116 -55
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +423 -252
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +7 -30
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +170 -193
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +52 -158
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +363 -99
- data/lib/phronomy.rb +10 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +25 -33
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -193
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- 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
|
-
|
|
29
|
-
# Replace with application-owned asynchronous work.
|
|
30
|
-
100
|
|
31
|
-
end
|
|
38
|
+
operation = start_import_async
|
|
32
39
|
|
|
33
|
-
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
+
_check_event_loop_reentrancy(:approve, :approve_async)
|
|
102
102
|
approve_async(
|
|
103
103
|
execution_id,
|
|
104
104
|
approval_request_id: approval_request_id,
|