phronomy 0.16.0 → 0.18.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/.mutant.yml +8 -9
- data/CHANGELOG.md +151 -1134
- data/CONTRIBUTING.md +28 -16
- data/README.md +69 -1249
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +4 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +26 -6
- data/benchmark/bench_tool_schema.rb +2 -35
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/docs/features.md +87 -0
- data/docs/getting-started.md +351 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/runtime-and-concurrency.md +258 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation.rb +2 -36
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
- data/lib/phronomy/agent/agent_root.rb +1 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +170 -333
- data/lib/phronomy/agent/context/capability/base.rb +177 -298
- data/lib/phronomy/agent/context_assembler.rb +65 -29
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
- data/lib/phronomy/agent/context_plan_validator.rb +0 -33
- data/lib/phronomy/agent/execution_coordinator.rb +6 -7
- data/lib/phronomy/agent/journal_projection.rb +28 -2
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
- data/lib/phronomy/agent/shared_state.rb +48 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +180 -336
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/agent.rb +6 -10
- data/lib/phronomy/configuration.rb +4 -171
- 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 +51 -79
- 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 +89 -190
- 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 +49 -377
- 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/llm_context_window/token_budget.rb +8 -79
- 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 +252 -256
- data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
- 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/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_runner.rb +2 -4
- data/lib/phronomy.rb +7 -121
- data/scripts/api_snapshot.rb +4 -15
- metadata +24 -38
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
- 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 -265
- 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/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Superseded by ADR-013.
|
|
6
|
+
|
|
7
|
+
This ADR records the historical decision for the former KnowledgeSource-based
|
|
8
|
+
architecture. `static_knowledge`, `KnowledgeSource`, `StaticKnowledge`,
|
|
9
|
+
`EntityKnowledge` and the class-level Knowledge cache are no longer part of the
|
|
10
|
+
active design.
|
|
6
11
|
|
|
7
12
|
## Context
|
|
8
13
|
|
|
@@ -43,3 +48,9 @@ re-fetched on each invocation because their content depends on runtime state.
|
|
|
43
48
|
non-static knowledge source.
|
|
44
49
|
- In tests, the cache must be cleared between examples. `Phronomy.reset_runtime!`
|
|
45
50
|
handles this.
|
|
51
|
+
|
|
52
|
+
## Supersession
|
|
53
|
+
|
|
54
|
+
ADR-013 replaces the source-object/cache model with Journal-backed persistent
|
|
55
|
+
Knowledge selected through Context Policy. The historical rationale above is
|
|
56
|
+
retained only to explain the superseded architecture.
|
|
@@ -2,51 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- Ractor-based isolation (if ever needed for security sandboxing) would require
|
|
52
|
-
significant API changes to `WorkflowContext` and RubyLLM integration.
|
|
5
|
+
**Superseded by ADR-010 on 2026-08-13.**
|
|
6
|
+
|
|
7
|
+
This file is retained as decision history. Its original implementation — one OS
|
|
8
|
+
Thread per `dispatch_parallel` child — is no longer the active architecture.
|
|
9
|
+
|
|
10
|
+
## Historical context
|
|
11
|
+
|
|
12
|
+
The original Orchestrator implementation needed concurrent subagent execution
|
|
13
|
+
before Phronomy had a common EventLoop/FSMSession control model. It therefore
|
|
14
|
+
used `Thread.new` per child Agent, optionally bounded by `max_concurrency`.
|
|
15
|
+
|
|
16
|
+
That choice provided straightforward parallelism for I/O-heavy Agent calls but
|
|
17
|
+
also tied logical child concurrency directly to OS-thread count.
|
|
18
|
+
|
|
19
|
+
## Superseding decision
|
|
20
|
+
|
|
21
|
+
Phronomy now models fan-out as a parent `FanOutInvocation` / FSMSession:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
FanOut FSMSession
|
|
25
|
+
|
|
|
26
|
+
+-- start child Agent A asynchronously
|
|
27
|
+
+-- start child Agent B asynchronously
|
|
28
|
+
+-- start child Agent C asynchronously
|
|
29
|
+
|
|
|
30
|
+
+-- child completion events
|
|
31
|
+
|
|
|
32
|
+
+-- aggregate / fail / timeout / cancel
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`max_concurrency` limits how many child Agent invocations are active. It does
|
|
36
|
+
not create a corresponding set of Threads.
|
|
37
|
+
|
|
38
|
+
Each child Agent runs through the common Agent FSMSession/EventLoop lifecycle.
|
|
39
|
+
When a child performs synchronous work that must not run on EventLoop, only that
|
|
40
|
+
synchronous operation is executed on the bounded `OffloadPool`.
|
|
41
|
+
|
|
42
|
+
Therefore:
|
|
43
|
+
|
|
44
|
+
- `dispatch_parallel` does not create raw Threads;
|
|
45
|
+
- no `Runtime#spawn` or Task execution backend is involved;
|
|
46
|
+
- child completion is delivered by EventLoop events;
|
|
47
|
+
- result ordering, fail/skip policy, timeout, and cancellation belong to the
|
|
48
|
+
FanOut state machine.
|
|
49
|
+
|
|
50
|
+
See ADR-010 for the current concurrency model and Thread boundary.
|
|
@@ -1,248 +1,175 @@
|
|
|
1
|
-
# ADR-010:
|
|
1
|
+
# ADR-010: EventLoop / FSMSession First Concurrency
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Accepted —
|
|
6
|
-
production-cooperative roadmap (Issues #331, #332, #334).
|
|
5
|
+
Accepted — revised for the OffloadPool execution boundary.
|
|
7
6
|
|
|
8
7
|
## Context
|
|
9
8
|
|
|
10
|
-
Phronomy
|
|
9
|
+
Phronomy must support many concurrently waiting Agent, Workflow, ToolInvocation,
|
|
10
|
+
and MultiAgent lifecycles without allocating one OS Thread per logical task.
|
|
11
|
+
At the same time, application and third-party code may contain synchronous work
|
|
12
|
+
that must not execute on the single Runtime EventLoop thread.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
a deprecated alias for `:immediate`.)*
|
|
17
|
-
- **`Phronomy::EventLoop`** — singleton event dispatcher; drives the cooperative
|
|
18
|
-
task cycle.
|
|
19
|
-
|
|
20
|
-
Early implementations occasionally forced `ThreadBackend` inside framework
|
|
21
|
-
components (e.g., `Agent::FSM#spawn_agent_task` in commit `0cb8510`) to work
|
|
22
|
-
around a perceived limitation: if an agent makes a blocking I/O call with the
|
|
23
|
-
cooperative backend active, it blocks the calling thread.
|
|
24
|
-
|
|
25
|
-
This approach was identified as wrong for two reasons:
|
|
26
|
-
|
|
27
|
-
1. **Misplaced responsibility** — threading the *application's* I/O at the
|
|
28
|
-
*framework* layer prevents the `runtime_backend` configuration from being
|
|
29
|
-
honored and removes the app developer's control over the concurrency model.
|
|
30
|
-
2. **Violation of layering** — the framework should not assume that its callers
|
|
31
|
-
are cooperative-scheduler-aware; it should provide primitives and let the
|
|
32
|
-
caller decide the backend.
|
|
33
|
-
|
|
34
|
-
There is a separate, legitimate category of blocking I/O: third-party gems
|
|
35
|
-
(RubyLLM, ActiveRecord, Redis, Faraday, etc.) that perform blocking system
|
|
36
|
-
calls internally and cannot be made non-blocking from the Phronomy side.
|
|
37
|
-
These must be handled differently from application-controlled I/O.
|
|
14
|
+
Classifying arbitrary application work as I/O-bound, CPU-bound, or external
|
|
15
|
+
process work is not a responsibility the framework can reliably infer. The
|
|
16
|
+
architecturally relevant distinction for Phronomy is whether a unit of work can
|
|
17
|
+
safely run to completion on EventLoop or must be moved off the control thread.
|
|
38
18
|
|
|
39
19
|
## Decision
|
|
40
20
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
21
|
+
Framework lifecycle coordination uses one Runtime-owned EventLoop and explicit
|
|
22
|
+
FSMSession state/events. **Task is a completion handle**, not an execution
|
|
23
|
+
backend.
|
|
24
|
+
|
|
25
|
+
Phronomy defines two Tool execution modes:
|
|
26
|
+
|
|
27
|
+
- `:cooperative` — short, EventLoop-safe work, or a specialized asynchronous
|
|
28
|
+
implementation that starts another Phronomy lifecycle and immediately returns
|
|
29
|
+
a completion handle.
|
|
30
|
+
- `:offloaded` — synchronous work that must not run on EventLoop. It executes in
|
|
31
|
+
the bounded `OffloadPool`.
|
|
32
|
+
|
|
33
|
+
`OffloadPool` is the thread execution boundary for synchronous work that must be
|
|
34
|
+
kept off EventLoop. It may contain blocking I/O, CPU-bound Ruby work, or other
|
|
35
|
+
application-defined long-running synchronous calls.
|
|
36
|
+
|
|
37
|
+
Workload classification such as I/O-bound versus CPU-bound is application-owned.
|
|
38
|
+
Phronomy does not provide separate `:blocking_io`, `:cpu_bound`, or
|
|
39
|
+
`:external_process` Tool execution modes.
|
|
40
|
+
|
|
41
|
+
Logical waits are never offloaded merely to obtain concurrency. Waiting for an
|
|
42
|
+
Agent, Workflow, ToolInvocation, approval, timer, or another Task is represented
|
|
43
|
+
as FSMSession state plus a later EventLoop event.
|
|
44
|
+
|
|
45
|
+
## Runtime model
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
Runtime
|
|
49
|
+
├─ EventLoop (one control-plane OS Thread)
|
|
50
|
+
│ └─ FSMSession
|
|
51
|
+
│ ├─ Agent
|
|
52
|
+
│ ├─ Workflow
|
|
53
|
+
│ ├─ ToolInvocation
|
|
54
|
+
│ └─ MultiAgent fan-out
|
|
55
|
+
├─ OffloadPool (bounded OS Threads)
|
|
56
|
+
│ ├─ blocking I/O
|
|
57
|
+
│ ├─ CPU-bound synchronous work
|
|
58
|
+
│ └─ other long synchronous work
|
|
59
|
+
├─ named OffloadPools
|
|
60
|
+
└─ EventLoop-driven timers
|
|
61
|
+
|
|
62
|
+
Task = completion handle
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
## Capacity and starvation
|
|
66
|
+
|
|
67
|
+
The default OffloadPool is a shared bounded resource. CPU-heavy work can occupy
|
|
68
|
+
slots that would otherwise be available to I/O, and slow I/O can do the same in
|
|
69
|
+
reverse. Phronomy guarantees bounded worker count, bounded queue depth,
|
|
70
|
+
backpressure, timeout/cancellation settlement, abandoned-worker accounting,
|
|
71
|
+
metrics, and lifecycle shutdown. It does **not** guarantee work-class fairness,
|
|
72
|
+
CPU isolation, core reservation, or CPU-bound speedup.
|
|
73
|
+
|
|
74
|
+
Applications own capacity planning through `offload_pool_size` and
|
|
75
|
+
`offload_queue_size`. Where isolation is required, applications may use
|
|
76
|
+
`Runtime#pool(name, size:, queue_size:)` to create independent named OffloadPool
|
|
77
|
+
resource domains.
|
|
78
|
+
|
|
79
|
+
## EventLoop admission rule
|
|
80
|
+
|
|
81
|
+
An EventLoop action must not block while waiting for a free OffloadPool queue
|
|
82
|
+
slot. Framework-owned EventLoop-origin submissions therefore use non-blocking
|
|
83
|
+
admission (`on_full: :raise`) and propagate `BackpressureError` through the
|
|
84
|
+
normal FSM/completion path.
|
|
85
|
+
|
|
86
|
+
External management threads may deliberately choose other admission policies
|
|
87
|
+
when blocking the caller is acceptable.
|
|
88
|
+
|
|
89
|
+
## Timeout and cancellation
|
|
90
|
+
|
|
91
|
+
An OffloadPool submit-time timeout settles the caller-facing PendingOperation.
|
|
92
|
+
It does not asynchronously interrupt a running worker Thread. If execution has
|
|
93
|
+
already started, the operation becomes abandoned, the worker may continue until
|
|
94
|
+
the submitted synchronous call returns, and that eventual worker result is
|
|
95
|
+
discarded.
|
|
96
|
+
|
|
97
|
+
The cancellation token passed to `OffloadPool#submit` follows the same
|
|
98
|
+
caller-facing settlement model:
|
|
99
|
+
|
|
100
|
+
- cancellation before worker execution prevents the submitted block from
|
|
101
|
+
starting;
|
|
102
|
+
- cancellation after worker execution starts settles the caller-facing
|
|
103
|
+
PendingOperation immediately, marks the operation abandoned, and allows the
|
|
104
|
+
worker to continue until the synchronous call returns;
|
|
105
|
+
- cancellation does not use `Thread#raise`;
|
|
106
|
+
- application code may observe the same CancellationToken and terminate its own
|
|
107
|
+
synchronous operation cooperatively.
|
|
108
|
+
|
|
109
|
+
A submit token with a monotonic deadline is connected to the Runtime timer queue,
|
|
110
|
+
so deadline expiry becomes explicit cancellation without a polling Thread.
|
|
111
|
+
|
|
112
|
+
`PendingOperation#blocking_wait(timeout:)` is a low-level synchronous bridge for
|
|
113
|
+
non-EventLoop callers such as tests and diagnostics. Its timeout is waiter-local:
|
|
114
|
+
it raises `TimeoutError` only to that caller and does not settle the
|
|
115
|
+
PendingOperation, cancel the submitted operation, or mark it abandoned.
|
|
116
|
+
PendingOperation does not define a waiter-local cancellation token;
|
|
117
|
+
operation-wide cancellation is represented only by the token passed to
|
|
118
|
+
`OffloadPool#submit`.
|
|
119
|
+
|
|
120
|
+
Independent notification callbacks are fault-isolated. A `StandardError` from one
|
|
121
|
+
`CancellationToken#on_cancel`, `Task#on_complete`, or
|
|
122
|
+
`PendingOperation#on_complete` subscriber is logged and does not suppress later
|
|
123
|
+
subscribers. This rule applies to notification fan-out; continuation or
|
|
124
|
+
transformation callbacks still report their own failures through the operation
|
|
125
|
+
they construct.
|
|
126
|
+
|
|
127
|
+
`abandoned_count` and the exported `offload_pool_abandoned_total` metric are
|
|
128
|
+
cumulative: they count operations whose caller-facing submit timeout or submit
|
|
129
|
+
cancellation settled after worker execution had already started.
|
|
130
|
+
`abandoned_active_count` and `offload_pool_abandoned_active` are current-state
|
|
131
|
+
values: they count only abandoned operations whose synchronous worker is still
|
|
132
|
+
occupying OffloadPool capacity.
|
|
133
|
+
|
|
134
|
+
## CPU-bound work
|
|
135
|
+
|
|
136
|
+
CPU-bound work is allowed through `:offloaded`. Thread offload protects the
|
|
137
|
+
EventLoop from direct long synchronous execution but does not remove CRuby GVL
|
|
138
|
+
contention or physical CPU contention. Those are explicitly outside the core
|
|
139
|
+
OffloadPool guarantee.
|
|
140
|
+
|
|
141
|
+
A future subprocess capability may provide process isolation, hard process
|
|
142
|
+
termination, stdout/stderr capture, and CPU-worker separation. That future
|
|
143
|
+
implementation belongs to the offload subsystem and does not reintroduce a
|
|
144
|
+
Tool-level `:external_process` execution class.
|
|
145
|
+
|
|
146
|
+
## Prohibited pattern
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
OffloadPool worker
|
|
150
|
+
→ child_agent.invoke_async
|
|
151
|
+
→ wait_result
|
|
80
152
|
```
|
|
81
153
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| Component | Reason |
|
|
85
|
-
|---|---|
|
|
86
|
-
| `EventLoop#start` | `run_loop` is the framework's own `while @running` infinite dispatch loop; running it on the shared scheduler would consume the scheduler, preventing all other tasks from running |
|
|
87
|
-
|
|
88
|
-
**Handler constraints for EventLoop:**
|
|
89
|
-
|
|
90
|
-
- Handler code runs **on the EventLoop thread**. Do not perform blocking
|
|
91
|
-
operations (database, LLM, HTTP) directly inside a handler — this stalls all
|
|
92
|
-
session processing.
|
|
93
|
-
- Do **not** call `Workflow#invoke` from within a handler. That call blocks
|
|
94
|
-
until the EventLoop processes events, causing a deadlock. Use the async
|
|
95
|
-
pattern: schedule work via `Runtime.instance.spawn` or `BlockingAdapterPool`,
|
|
96
|
-
then post results back with `EventLoop#post`.
|
|
97
|
-
|
|
98
|
-
All other framework components — including FSM, orchestration, RAG, streaming —
|
|
99
|
-
do NOT own an infinite loop and therefore MUST use `Runtime.instance.spawn`.
|
|
100
|
-
|
|
101
|
-
### Rule 3 — BlockingAdapterPool for uncontrollable blocking I/O
|
|
154
|
+
and equivalently for Workflow/ToolInvocation/Task lifecycles.
|
|
102
155
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
`BlockingAdapterPool`.
|
|
156
|
+
That pattern converts a logical wait into worker-slot occupancy and can create
|
|
157
|
+
pool starvation. The correct model is:
|
|
106
158
|
|
|
159
|
+
```text
|
|
160
|
+
parent FSMSession
|
|
161
|
+
→ start child lifecycle
|
|
162
|
+
→ return completion handle immediately
|
|
163
|
+
→ child settles
|
|
164
|
+
→ post parent EventLoop event
|
|
107
165
|
```
|
|
108
|
-
┌─────────────────────────────────────┐
|
|
109
|
-
│ Cooperative EventLoop / Runtime │
|
|
110
|
-
│ (FakeScheduler, ImmediateBackend) │
|
|
111
|
-
│ │
|
|
112
|
-
│ Agent ──► BlockingAdapterPool ──► RubyLLM (HTTP)
|
|
113
|
-
│ RAG ──► BlockingAdapterPool ──► ActiveRecord / Redis
|
|
114
|
-
└─────────────────────────────────────┘
|
|
115
|
-
│
|
|
116
|
-
┌─────────┴─────────┐
|
|
117
|
-
│ Thread pool │ ← bounded (max_threads:)
|
|
118
|
-
│ (OS threads) │
|
|
119
|
-
└───────────────────┘
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Properties of `BlockingAdapterPool`:
|
|
123
|
-
- Uses OS threads internally (they are the correct tool for I/O that releases
|
|
124
|
-
the GVL).
|
|
125
|
-
- **Always bounded** — configurable `max_threads:` (no unbounded `Thread.new`).
|
|
126
|
-
- Returns a `Phronomy::Task`-compatible future so the cooperative layer can
|
|
127
|
-
await results without blocking the EventLoop.
|
|
128
|
-
- Is the **only** place in the framework that creates raw threads for I/O.
|
|
129
|
-
|
|
130
|
-
> **Note:** `dispatch_parallel` (ADR-008) currently creates one thread per
|
|
131
|
-
> sub-agent via `Thread.new`. This predates the `BlockingAdapterPool` concept
|
|
132
|
-
> and is a candidate for future migration. Until that migration, it remains an
|
|
133
|
-
> accepted exception per ADR-008.
|
|
134
|
-
|
|
135
|
-
## Scheduler Landscape (as of 2026-05-25)
|
|
136
|
-
|
|
137
|
-
Three scheduler backends currently exist:
|
|
138
|
-
|
|
139
|
-
| Symbol / Class | Status | Purpose |
|
|
140
|
-
|---|---|---|
|
|
141
|
-
| `:thread` — `ThreadScheduler` / `ThreadBackend` | **Production default** | One OS thread per task; GVL-releasing I/O works transparently |
|
|
142
|
-
| `:immediate` — `FakeScheduler` / `ImmediateBackend` | Test / CI | Synchronous; block runs to completion before `spawn` returns; no threads |
|
|
143
|
-
| `:fiber` — `DeterministicScheduler` / `FiberBackend` | **Experimental validation backend** | Tick-based Fiber scheduler with virtual clock; enables deterministic concurrency tests without wall-clock timers. Named backend added in Issue #334. |
|
|
144
|
-
|
|
145
|
-
### `:cooperative` deprecation
|
|
146
|
-
|
|
147
|
-
`:cooperative` was a silent alias for `:immediate` (mapped to `FakeScheduler`).
|
|
148
|
-
As of Issue #332, it now emits a `WARN`-level deprecation message and must not
|
|
149
|
-
be used in new code. Issue #334 introduced `:fiber` as the named experimental
|
|
150
|
-
validation backend; `:cooperative` is retained only for backwards compatibility.
|
|
151
|
-
|
|
152
|
-
### DeterministicScheduler as a stepping stone
|
|
153
|
-
|
|
154
|
-
`DeterministicScheduler` is the foundation for the long-term production
|
|
155
|
-
cooperative runtime goal. It provides:
|
|
156
|
-
|
|
157
|
-
- Ready-queue dispatch, virtual timer heap, scheduler signal API
|
|
158
|
-
- `FiberBackend` wrapping tasks as Fibers
|
|
159
|
-
- `tick` / `run_until_idle` / `advance(seconds)` for deterministic test control
|
|
160
|
-
|
|
161
|
-
**Remaining gaps versus a full production cooperative runtime:**
|
|
162
|
-
|
|
163
|
-
- No real-wall-clock timer integration (`TimerQueue` runs as a background thread — Issue #331)
|
|
164
|
-
- No `BlockingAdapterPool` integration for LLM/network calls (Issue #280)
|
|
165
|
-
- Scheduler must be driven manually (`tick`) rather than event-loop-driven
|
|
166
|
-
|
|
167
|
-
The minimum delta to promote `DeterministicScheduler` to production use:
|
|
168
|
-
|
|
169
|
-
1. Integrate `TimerQueue` into the scheduler `tick` cycle (eliminates timer thread — Issue #331)
|
|
170
|
-
2. Implement `BlockingAdapterPool` and wire LLM/tool calls through it (Issue #280)
|
|
171
|
-
3. ~~Expose as a named `runtime_backend` (`:fiber`)~~ — **done** (Issue #334)
|
|
172
|
-
4. Add real-wall-clock integration (replace virtual time with `Process.clock_gettime`)
|
|
173
|
-
|
|
174
|
-
Progress is tracked in Issues #331 and #280.
|
|
175
|
-
|
|
176
|
-
### TimerQueue background thread
|
|
177
|
-
|
|
178
|
-
`Runtime::TimerQueue` currently runs as a dedicated background OS thread
|
|
179
|
-
(`Thread.new { run_loop }`). This is an accepted interim implementation — it is
|
|
180
|
-
robust for production but violates the cooperative-first goal. The long-term
|
|
181
|
-
target is to integrate timer firing into the scheduler's own `tick` cycle so
|
|
182
|
-
that no separate timer thread is needed. Progress tracked in Issue #331.
|
|
183
166
|
|
|
184
167
|
## Consequences
|
|
185
168
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
-
|
|
189
|
-
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
-
|
|
193
|
-
only where true concurrency is required.
|
|
194
|
-
- `DeterministicScheduler` + `FiberBackend` provide a deterministic test
|
|
195
|
-
foundation for cooperative concurrency without wall-clock timers.
|
|
196
|
-
|
|
197
|
-
### Negative / Tradeoffs
|
|
198
|
-
- `BlockingAdapterPool` is not yet implemented (as of ADR-010 acceptance).
|
|
199
|
-
Until it is, callers that need real blocking I/O must opt in to `:thread`
|
|
200
|
-
backend explicitly (e.g., in tests via `around` blocks).
|
|
201
|
-
- `dispatch_parallel` remains on raw threads (ADR-008) until migrated.
|
|
202
|
-
- `TimerQueue` runs as a background OS thread (interim; see Issue #331).
|
|
203
|
-
- `DeterministicScheduler` backs the named `:fiber` backend (Issue #334
|
|
204
|
-
resolved). It remains experimental and not for production use; the remaining
|
|
205
|
-
steps toward a full production cooperative runtime are `TimerQueue`
|
|
206
|
-
integration (Issue #331) and `BlockingAdapterPool` completion (Issue #280).
|
|
207
|
-
|
|
208
|
-
## Derived Checklist
|
|
209
|
-
|
|
210
|
-
When writing or reviewing any Phronomy component, apply this checklist:
|
|
211
|
-
|
|
212
|
-
| Question | Correct action |
|
|
213
|
-
|---|---|
|
|
214
|
-
| Does this component own a `while true` / `loop do` that blocks forever? | May use dedicated `ThreadScheduler` (Rule 2) |
|
|
215
|
-
| Does this component call into app/agent code (LLM, tools)? | `Runtime.instance.spawn` — no ThreadBackend (Rule 1) |
|
|
216
|
-
| Does this component call a blocking gem (RubyLLM, AR, Redis)? | Route through `BlockingAdapterPool` (Rule 3) |
|
|
217
|
-
| Is this a test that needs real concurrency (sleep + cancel)? | `around` block with `c.runtime_backend = :thread` opt-in |
|
|
218
|
-
| Anything else | Default: `Runtime.instance.spawn` |
|
|
219
|
-
|
|
220
|
-
## Related ADRs and Issues
|
|
221
|
-
|
|
222
|
-
- ADR-008: Orchestrator Uses OS Threads for Parallel Dispatch (predates this
|
|
223
|
-
ADR; `dispatch_parallel` is a future migration candidate)
|
|
224
|
-
- Issue #331: TimerQueue scheduler integration (long-term — eliminate timer thread)
|
|
225
|
-
- Issue #334: Promote DeterministicScheduler to production cooperative runtime
|
|
226
|
-
- Issue #332: `:cooperative` alias deprecation (resolved 2026-05-25)
|
|
227
|
-
- Issue #280: MCP transports behind BlockingAdapterPool (pending)
|
|
228
|
-
|
|
229
|
-
## Non-goals
|
|
230
|
-
|
|
231
|
-
The following capabilities are intentionally out of scope for this framework's
|
|
232
|
-
concurrency layer:
|
|
233
|
-
|
|
234
|
-
- **CPU-bound process pool** — A `ProcessPoolExecutor` equivalent is not part
|
|
235
|
-
of core framework by default. CPU-intensive tool work belongs at the
|
|
236
|
-
application layer (fork, Sidekiq, etc.). A separate ADR would be required
|
|
237
|
-
to introduce one.
|
|
238
|
-
- **External process manager** — Spawning, monitoring, or restarting external
|
|
239
|
-
subprocesses is not currently a framework responsibility. A separate ADR
|
|
240
|
-
would be required.
|
|
241
|
-
- **Preemptive scheduling** — The cooperative-first model is non-preemptive by
|
|
242
|
-
design. Introducing a preemptive scheduler or promoting `:fiber` to
|
|
243
|
-
production default is not currently planned; a separate ADR would be required.
|
|
244
|
-
- **Additional ToolExecutor core execution routes** — Only `:cooperative` and
|
|
245
|
-
`:blocking_io` are core dispatch routes. `:cpu_bound` and `:external_process`
|
|
246
|
-
are compatibility aliases that fall back to `:blocking_io` with a warning.
|
|
247
|
-
Any genuinely new core execution route requires a new ADR.
|
|
248
|
-
|
|
169
|
+
- There is one explicit framework continuation model: FSMSession + EventLoop.
|
|
170
|
+
- `Task` stays thread-free and represents settlement only.
|
|
171
|
+
- Tool execution classification becomes `:cooperative` / `:offloaded`.
|
|
172
|
+
- CPU/I/O classification and resource sizing are application responsibilities.
|
|
173
|
+
- Named pools remain available for application-managed resource isolation.
|
|
174
|
+
- Production Fiber execution is not part of the architecture.
|
|
175
|
+
- Raw production Threads remain confined to EventLoop and OffloadPool.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Superseded by ADR-012 — 2026-08-08
|
|
6
6
|
|
|
7
7
|
Originally proposed — 2026-05-31.
|
|
8
8
|
|
|
9
9
|
## Supersession Note
|
|
10
10
|
|
|
11
|
-
ADR-012, **Canonical Complete Execution Log and Context Policy**, supersedes
|
|
11
|
+
ADR-012, **Canonical Complete Execution Log and Context Policy**, supersedes this ADR as an active architecture contract. This document is retained as historical design analysis; references below to `build_context`, `context_overhead`, and `LlmContextWindow::Assembler` are non-normative.
|
|
12
12
|
|
|
13
13
|
In particular, the following parts of this ADR are no longer normative for the stateful Agent architecture:
|
|
14
14
|
|