phronomy 0.25.0 → 0.26.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 +31 -0
- data/CONTRIBUTING.md +4 -4
- data/README.md +10 -7
- data/benchmark/bench_regression.rb +1 -1
- data/docs/architecture/agent-context.md +1 -1
- data/docs/architecture/persistence.md +2 -2
- data/docs/architecture/tracing.md +3 -3
- data/docs/async-composition.md +205 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
- data/docs/features.md +12 -9
- data/docs/getting-started.md +6 -6
- data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
- data/docs/persistence-backends.md +1 -1
- data/docs/runtime-and-concurrency.md +89 -29
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
- data/lib/phronomy/agent/base.rb +4 -10
- data/lib/phronomy/agent/exact_execution.rb +3 -3
- data/lib/phronomy/agent/execution_coordinator.rb +20 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
- data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
- data/lib/phronomy/agent/tool_executor.rb +4 -4
- data/lib/phronomy/agent/tool_invocation.rb +1 -1
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
- data/lib/phronomy/blocking.rb +15 -8
- data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
- data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
- data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
- data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
- data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
- data/lib/phronomy/engine/event_loop.rb +7 -7
- data/lib/phronomy/engine/fsm_session.rb +3 -3
- data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
- data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
- data/lib/phronomy/execution.rb +236 -0
- data/lib/phronomy/execution_cancellation_error.rb +12 -0
- data/lib/phronomy/execution_timeout_error.rb +12 -0
- data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
- data/lib/phronomy/invocation_context.rb +13 -1
- data/lib/phronomy/llm_adapter/base.rb +2 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
- data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
- data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
- data/lib/phronomy/testing/fake_clock.rb +13 -9
- data/lib/phronomy/tools/agent.rb +5 -5
- data/lib/phronomy/vector_store/async_backend.rb +5 -5
- data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
- data/lib/phronomy/workflow.rb +2 -2
- data/lib/phronomy/workflow_runner.rb +4 -4
- data/scripts/api_snapshot.rb +3 -1
- data/sig/phronomy/agent.rbs +4 -4
- data/sig/phronomy/extensions.rbs +5 -5
- data/sig/phronomy/multi_agent.rbs +1 -3
- data/sig/phronomy/runtime.rbs +34 -7
- data/sig/phronomy/tool.rbs +1 -1
- data/sig/phronomy/workflow.rbs +1 -1
- metadata +11 -5
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd6beeb52633381ff9e249ec18a1dc422234c78e44f479e3549cd3c54591ab04
|
|
4
|
+
data.tar.gz: 6528144759933b359d2303b1499b678cbd4eb0c747ca9966f0c6542effb28196
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 307b601d09f6113282eef52f3e547d259e728adf0ba9ac481e3d84dc99583b8510ad779c71e0abee8c78f180b3786d6eacb3fea66723432d93f523e7f4be756a
|
|
7
|
+
data.tar.gz: 6aed95a5486eaaa792e25ad4d058ebc804278dfa2bf648f216d120256154f7f1cccba86c056ec5176f98556f16e7b835c7102dd0129da5474f9ce93f221cbae8
|
data/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,37 @@ Release history for 0.14.0 and earlier is archived in
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [0.26.0] - 2026-09-14
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Rename the public completion type to `Phronomy::TaskResult`; remove `Task`
|
|
22
|
+
without a compatibility alias in this development release.
|
|
23
|
+
- Remove `Orchestrator#fan_out` and `#fan_out_async`. Use `dispatch_parallel`
|
|
24
|
+
for Agent-specific construction/knowledge/concurrency policy, or Execution
|
|
25
|
+
for application-defined JOB composition.
|
|
26
|
+
- Preserve cancelled states and original cancellation errors across result
|
|
27
|
+
transformations, independently of failures that carry CancellationError.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `TaskResult#flat_map`, `.all_settled`, and immutable input-order Outcome records.
|
|
32
|
+
- `Execution.run_async` / `.run` for shared runtime fan-out/fan-in, explicit
|
|
33
|
+
invocation context, whole-execution timeout/cancellation and partial snapshots.
|
|
34
|
+
- `Execution#observe` for scoped continuations of externally owned results.
|
|
35
|
+
- Optional `invocation_context:` on `Blocking.call_async`, with one-way combined
|
|
36
|
+
context and individual controls shared with Agent admission.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Dispose finished execution subscriptions and timers while retaining required
|
|
41
|
+
physical completion tracking. Orchestrator reuses the common Execution engine.
|
|
42
|
+
|
|
43
|
+
See [Result composition and Execution](docs/async-composition.md) for migration,
|
|
44
|
+
completion boundaries, cancellation and application responsibilities.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
17
48
|
## [0.25.0] - 2026-09-12
|
|
18
49
|
|
|
19
50
|
### Added
|
data/CONTRIBUTING.md
CHANGED
|
@@ -98,16 +98,16 @@ Do not change Ruby visibility merely to make it match an `@api` annotation.
|
|
|
98
98
|
|
|
99
99
|
### Async completion boundary
|
|
100
100
|
|
|
101
|
-
`Phronomy::
|
|
101
|
+
`Phronomy::TaskResult` is the caller-facing completion handle. EventLoop/FSMSession and
|
|
102
102
|
OffloadPool are execution/continuation mechanisms, not competing completion
|
|
103
103
|
abstractions.
|
|
104
104
|
|
|
105
105
|
Synchronous work that requires execution away from EventLoop must use an
|
|
106
106
|
OffloadPool. Do not create production worker Threads in adapters, backends, or
|
|
107
107
|
Tools to emulate asynchronous behavior. Logical waits between Phronomy
|
|
108
|
-
lifecycles stay on EventLoop/FSMSession and settle a
|
|
108
|
+
lifecycles stay on EventLoop/FSMSession and settle a TaskResult later.
|
|
109
109
|
|
|
110
|
-
The framework owns
|
|
110
|
+
The framework owns TaskResult settlement (`complete`, `fail`, and framework-driven
|
|
111
111
|
cancellation). Application code should observe Tasks through `wait_result`,
|
|
112
112
|
`on_complete`, `map`, and settlement state. Operation-wide cancellation of
|
|
113
113
|
OffloadPool work is supplied through `CancellationToken`.
|
|
@@ -149,7 +149,7 @@ ambiguous. Do not renumber historical ADRs to repair that legacy collision.
|
|
|
149
149
|
|
|
150
150
|
For Agent Context work, ADR-012 and ADR-013 define the current Journal,
|
|
151
151
|
Manifest, Context Policy and persistent Knowledge model. ADR-010 defines the
|
|
152
|
-
EventLoop/FSMSession,
|
|
152
|
+
EventLoop/FSMSession, TaskResult, and OffloadPool execution boundary. ADR-015 defines
|
|
153
153
|
the Tool public façade, extension-SPI boundary, and RBS ownership rules.
|
|
154
154
|
|
|
155
155
|
Current explanatory architecture starts at [`docs/architecture.md`](docs/architecture.md).
|
data/README.md
CHANGED
|
@@ -22,8 +22,10 @@ tracking `main` directly.
|
|
|
22
22
|
- **Multi-Agent Handoff** — semantic Source-to-Target responsibility transfer with policy-bounded Context projection and persisted active responsibility and exact Target recovery within one Persistence domain.
|
|
23
23
|
- **EventLoop + FSMSession** — the framework control plane for logical lifecycle coordination.
|
|
24
24
|
- **OffloadPool** — bounded operating-system-thread execution boundary for synchronous work that must not run on EventLoop.
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
25
|
+
- **TaskResult** — the common thread-free completion handle returned by Phronomy asynchronous APIs, including OffloadPool-backed work.
|
|
26
|
+
- **TaskResult.completed / TaskResult.failed** — already-settled application results without starting execution.
|
|
27
|
+
- **TaskResult.map / flat_map / all_settled** — result transformation, asynchronous chaining and ordered all-settled observation.
|
|
28
|
+
- **Execution.run_async / run** — start application JOBs and join their final results under a whole-execution timeout and cancellation scope.
|
|
27
29
|
- **Blocking.call_async** — submits synchronous application work to the existing bounded OffloadPool without waiting for queue space.
|
|
28
30
|
- **Journal / Context Policy / Manifest** — canonical history plus per-LLM-call context selection.
|
|
29
31
|
|
|
@@ -86,8 +88,8 @@ class. The legacy `Phronomy::Agent::Context::Capability::Base` constant remains
|
|
|
86
88
|
valid for compatibility.
|
|
87
89
|
|
|
88
90
|
For non-blocking top-level use, call `invoke_async` and keep the returned
|
|
89
|
-
`Phronomy::
|
|
90
|
-
another
|
|
91
|
+
`Phronomy::TaskResult`. Inside Phronomy lifecycle callbacks, do not block waiting for
|
|
92
|
+
another TaskResult; continue through explicit events instead.
|
|
91
93
|
|
|
92
94
|
```ruby
|
|
93
95
|
task = ResearchAgent.new.invoke_async("Research Ruby AI frameworks")
|
|
@@ -128,14 +130,14 @@ Runtime
|
|
|
128
130
|
└─ EventLoop-driven timers
|
|
129
131
|
|
|
130
132
|
EventLoop / FSMSession ─┐
|
|
131
|
-
├─>
|
|
133
|
+
├─> TaskResult = completion handle
|
|
132
134
|
OffloadPool ────────────┘
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
Logical waiting remains in EventLoop/FSMSession state. Synchronous work that
|
|
136
138
|
would block EventLoop uses the bounded OffloadPool. OffloadPool-specific queue,
|
|
137
139
|
worker, timeout, and abandonment state remains private runtime machinery; callers
|
|
138
|
-
observe completion through `Phronomy::
|
|
140
|
+
observe completion through `Phronomy::TaskResult`. See
|
|
139
141
|
[Runtime and concurrency](docs/runtime-and-concurrency.md) for the detailed
|
|
140
142
|
contracts, timeout/cancellation semantics, metrics, and callback rules.
|
|
141
143
|
|
|
@@ -144,7 +146,8 @@ contracts, timeout/cancellation semantics, metrics, and callback rules.
|
|
|
144
146
|
- [Getting started](docs/getting-started.md) — installation, RubyLLM setup, Agent/Workflow basics, persistence, streaming.
|
|
145
147
|
- [Features and API stability](docs/features.md) — public feature matrix and stability labels.
|
|
146
148
|
- [Architecture](docs/architecture.md) — canonical current explanatory architecture entry and authority navigation.
|
|
147
|
-
- [Runtime and concurrency](docs/runtime-and-concurrency.md) — EventLoop, FSMSession,
|
|
149
|
+
- [Runtime and concurrency](docs/runtime-and-concurrency.md) — EventLoop, FSMSession, TaskResult, OffloadPool, cancellation, observability.
|
|
150
|
+
- [Result composition and Execution](docs/async-composition.md) — application JOBs, map/flat_map, fan-in snapshots, context ownership and migration.
|
|
148
151
|
- [MCP client](docs/mcp-client.md) — Model Context Protocol (MCP) integration and supported schema subset.
|
|
149
152
|
- [Migration from 0.15-era APIs](docs/migrations/0.15.md).
|
|
150
153
|
- [0.16 cleanup migration](docs/migrations/0.16.md).
|
|
@@ -102,7 +102,7 @@ stub_agent_class = Class.new(Phronomy::Agent::Base) do
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
define_method(:invoke_async) do |input, **_kw|
|
|
105
|
-
task = Phronomy::
|
|
105
|
+
task = Phronomy::TaskResult.deferred(name: "bench-stub")
|
|
106
106
|
begin
|
|
107
107
|
task.complete(invoke(input))
|
|
108
108
|
rescue => error
|
|
@@ -133,7 +133,7 @@ reconcilable
|
|
|
133
133
|
resolution_required
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
Recovery does not restore the old Ruby object graph or old
|
|
136
|
+
Recovery does not restore the old Ruby object graph or old TaskResult callbacks. It
|
|
137
137
|
reconstructs current logical state from durable evidence and continues only
|
|
138
138
|
when authority and outcome certainty permit it.
|
|
139
139
|
|
|
@@ -38,7 +38,7 @@ Durable examples include AgentRoot, Journal records, AgentExecution records,
|
|
|
38
38
|
content/Manifest references, and durable Workflow snapshots.
|
|
39
39
|
|
|
40
40
|
Runtime-only examples include FSMSession objects/IDs, AgentInvocation objects,
|
|
41
|
-
|
|
41
|
+
TaskResult instances/callbacks, EventLoop entries, Runtime admission entries, and
|
|
42
42
|
in-flight Provider/Tool operation objects.
|
|
43
43
|
|
|
44
44
|
Runtime/process loss removes Runtime-only objects but does not imply confirmed
|
|
@@ -85,7 +85,7 @@ The canonical logical/durable Workflow identity is `workflow_instance_id`, which
|
|
|
85
85
|
is separate from one Runtime `fsm_session_id`.
|
|
86
86
|
|
|
87
87
|
Workflow same-process admission is acquired before durable hydration and retained
|
|
88
|
-
through the authoritative terminal/halted save barrier. The caller-facing
|
|
88
|
+
through the authoritative terminal/halted save barrier. The caller-facing TaskResult
|
|
89
89
|
settles after that authoritative durable barrier.
|
|
90
90
|
|
|
91
91
|
See
|
|
@@ -75,7 +75,7 @@ The same principle applies to Workflow run/resume operations.
|
|
|
75
75
|
One active Agent run/resume segment.
|
|
76
76
|
|
|
77
77
|
The span starts after `execution_id` has been established and the active segment
|
|
78
|
-
is about to run. It ends when that segment's caller-facing
|
|
78
|
+
is about to run. It ends when that segment's caller-facing TaskResult settles.
|
|
79
79
|
|
|
80
80
|
Suspension closes the active span. Later approval/recovery continuation creates a
|
|
81
81
|
new `agent.execution` span correlated by the same logical `execution_id`.
|
|
@@ -85,7 +85,7 @@ execution.
|
|
|
85
85
|
|
|
86
86
|
### `workflow.execution`
|
|
87
87
|
|
|
88
|
-
One Workflow run/resume operation, ending with that operation's
|
|
88
|
+
One Workflow run/resume operation, ending with that operation's TaskResult settlement.
|
|
89
89
|
The durable identity is `workflow_instance_id`.
|
|
90
90
|
|
|
91
91
|
### `llm.call`
|
|
@@ -131,7 +131,7 @@ API, EventLoop span registry, or durable span state.
|
|
|
131
131
|
A backend such as OpenTelemetry may preserve natural lexical nesting where its
|
|
132
132
|
own context is active. That adapter behavior does not establish a Phronomy-wide
|
|
133
133
|
guarantee that automatic spans form one backend-native parent/child tree across
|
|
134
|
-
|
|
134
|
+
TaskResult/EventLoop/Offload/Runtime boundaries.
|
|
135
135
|
|
|
136
136
|
## 8. `trace_pii`
|
|
137
137
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Result composition and Execution
|
|
2
|
+
|
|
3
|
+
`Phronomy::TaskResult` represents a pending or settled result. It does not start
|
|
4
|
+
a thread. `Phronomy::Execution` starts the input JOBs and waits for the final
|
|
5
|
+
TaskResult returned by each JOB. The application defines those completion
|
|
6
|
+
conditions and what to do with the collected values.
|
|
7
|
+
|
|
8
|
+
## Invocation, individual results and fan-in
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
require "phronomy"
|
|
12
|
+
|
|
13
|
+
def collect_answers_async(agents, question:, invocation_context: nil)
|
|
14
|
+
Phronomy::Execution.run_async(
|
|
15
|
+
agents, timeout: 30, invocation_context: invocation_context
|
|
16
|
+
) do |agent, execution|
|
|
17
|
+
agent.invoke_async(question, invocation_context: execution.invocation_context)
|
|
18
|
+
.map { |response| response.fetch(:output).strip }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Each outcome identifies the original input position, including failed JOBs.
|
|
23
|
+
outcomes = collect_answers_async(agents, question: "Explain the tradeoff").wait_result
|
|
24
|
+
outcomes.each do |outcome|
|
|
25
|
+
puts [outcome.index, outcome.status, outcome.value, outcome.error&.message].inspect
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`agents` contains separately generated or loaded Agent instances. Use each
|
|
30
|
+
Agent's materialization listener for lifecycle/progress events. Result handling
|
|
31
|
+
uses the TaskResult returned by the invocation.
|
|
32
|
+
|
|
33
|
+
The input must be an Array. Its positions are shallowly copied before any JOB
|
|
34
|
+
starts. Start blocks run in input order without waiting for previous returned
|
|
35
|
+
results. They can run on the calling thread. Execution adds no per-JOB thread
|
|
36
|
+
or worker: keep start and transformation blocks short and use
|
|
37
|
+
`Blocking.call_async` for application work that can block. A start block itself
|
|
38
|
+
can delay the return of `run_async` if the application performs synchronous work.
|
|
39
|
+
|
|
40
|
+
Zero, one and many inputs all produce an Array of Outcome records. Empty input
|
|
41
|
+
still validates parameters and rejects an already-expired deadline or cancelled
|
|
42
|
+
token before completing. `Execution.run` starts the same execution once and
|
|
43
|
+
waits for its Outcome Array. The synchronous entrance is rejected on EventLoop
|
|
44
|
+
even for empty or already-completed inputs.
|
|
45
|
+
|
|
46
|
+
## map and flat_map
|
|
47
|
+
|
|
48
|
+
| Operation | Success callback | Derived result |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `result.map { ... }` | Returns any value | That value, even when the value itself is a TaskResult |
|
|
51
|
+
| `result.flat_map { ... }` | Returns a TaskResult | Waits for and adopts that inner result's terminal value/state |
|
|
52
|
+
| `result.on_complete { \|value, error\| ... }` | Independent completion notification | Returns the original result; the callback does not transform it |
|
|
53
|
+
|
|
54
|
+
A callback can run immediately during registration or on the completing thread.
|
|
55
|
+
There is no callback thread guarantee. `map`/`flat_map` block StandardError
|
|
56
|
+
failures become failures of the derived result, preserving the original error.
|
|
57
|
+
A wrong `flat_map` return becomes a TypeError failure. Missing blocks raise
|
|
58
|
+
ArgumentError at registration. A source or inner **cancelled state** propagates
|
|
59
|
+
as cancelled with the original error. A failed CancellationError, or one raised
|
|
60
|
+
by an application transformation, stays failed.
|
|
61
|
+
|
|
62
|
+
For example, extend each JOB through a second Agent before returning its result:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
evaluations = Phronomy::Execution.run_async(pairs, timeout: 30) do |pair, execution|
|
|
66
|
+
context = execution.invocation_context
|
|
67
|
+
pair.fetch(:author).invoke_async(question, invocation_context: context)
|
|
68
|
+
.flat_map do |answer|
|
|
69
|
+
pair.fetch(:reviewer).invoke_async(answer.fetch(:output), invocation_context: context)
|
|
70
|
+
.map { |review| review.fetch(:output) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If a JOB raises StandardError or returns something other than a TaskResult,
|
|
76
|
+
that JOB becomes failed. Other JOBs continue. Once all final results settle,
|
|
77
|
+
the whole execution succeeds with the Outcome Array even if it contains
|
|
78
|
+
individual failures or cancellations.
|
|
79
|
+
|
|
80
|
+
## Outcome records and whole-execution deadlines
|
|
81
|
+
|
|
82
|
+
`TaskResult::Outcome` has read-only `index`, `status`, `value` and `error` fields.
|
|
83
|
+
The returned Array and its records are frozen. Values and original exceptions
|
|
84
|
+
are retained by reference, without deep copying or freezing application objects.
|
|
85
|
+
|
|
86
|
+
| Whole execution | Whole TaskResult | Available records |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| All JOBs settled | completed | Successful value is the Outcome Array |
|
|
89
|
+
| `timeout:` won | failed, ExecutionTimeoutError | `error.outcomes` |
|
|
90
|
+
| Explicit or inherited token/deadline cancellation won | cancelled, ExecutionCancellationError | `error.outcomes` |
|
|
91
|
+
|
|
92
|
+
Record statuses are `:completed`, `:failed`, `:cancelled`, and, in an interrupted
|
|
93
|
+
snapshot, `:unfinished`. Unstarted JOBs also have unfinished records. This is
|
|
94
|
+
not another terminal state of TaskResult. A JOB's record refers to its final
|
|
95
|
+
returned result, so completed transformed values are retained; an unfinished
|
|
96
|
+
inner `flat_map` result makes that JOB unfinished.
|
|
97
|
+
|
|
98
|
+
The execution serializes record updates and terminal claims. The first terminal
|
|
99
|
+
claim wins. At timeout/cancellation it fixes the records before requesting child
|
|
100
|
+
cancellation; later notifications cannot rewrite those records. It does not
|
|
101
|
+
retroactively decide using physical worker completion timestamps.
|
|
102
|
+
|
|
103
|
+
`timeout:` covers fan-out start through the final JOB results reaching fan-in.
|
|
104
|
+
`nil` adds no deadline. Zero or negative values fail before starting JOBs. It
|
|
105
|
+
accepts finite real Numeric values usable as elapsed seconds; other types raise
|
|
106
|
+
TypeError, and non-finite/complex values raise ArgumentError. These checks occur
|
|
107
|
+
before JOB starts or cancellation subscriptions. The existing Blocking numeric
|
|
108
|
+
conversion rules are unchanged.
|
|
109
|
+
|
|
110
|
+
`cancellation_token:` accepts nil or a CancellationToken (including subclasses).
|
|
111
|
+
`invocation_context:` accepts nil or an existing InvocationContext (including
|
|
112
|
+
subclasses). Other types raise TypeError at the Execution entrance.
|
|
113
|
+
|
|
114
|
+
`TaskResult#wait_result(timeout:)` sets only that caller's wait limit. It does not
|
|
115
|
+
cancel or alter the underlying result. Pending waits are forbidden on EventLoop.
|
|
116
|
+
An individual Blocking timeout keeps its TimeoutError failure; an individual
|
|
117
|
+
deadline token keeps the cancellation state. These are distinct from the whole
|
|
118
|
+
execution's errors and snapshot.
|
|
119
|
+
|
|
120
|
+
## Context, cancellation and ownership
|
|
121
|
+
|
|
122
|
+
Pass `execution.invocation_context` explicitly to an Agent or to
|
|
123
|
+
`Blocking.call_async(invocation_context: ..., cancellation_token: ...)` for work
|
|
124
|
+
belonging to that execution. The result is bound before it returns to the app;
|
|
125
|
+
its `map`/`flat_map` continuations inherit the scope. A continuation checks the
|
|
126
|
+
scope before running. Explicitly passing the context again when starting an
|
|
127
|
+
inner operation also covers cancellation after the continuation has begun.
|
|
128
|
+
|
|
129
|
+
An optional existing context contributes its user, policy, budget and tracing
|
|
130
|
+
information by reference. Execution derives a new context without modifying
|
|
131
|
+
the supplied one. Existing context cancellation/deadline and the explicit
|
|
132
|
+
Execution token/timeout all remain effective. Controls connect one way into a
|
|
133
|
+
private token. Cancelling one execution or an individual operation does not
|
|
134
|
+
cancel a parent's or another caller's shared token. Agent and Blocking admission
|
|
135
|
+
combine the individual token with the context's controls.
|
|
136
|
+
|
|
137
|
+
Scope cancellation stops unstarted scoped operations/transformations and requests
|
|
138
|
+
cooperative cancellation of running operations. It does not use Thread#raise,
|
|
139
|
+
roll back effects, or guarantee physical worker termination. Offload work and
|
|
140
|
+
owned composition steps retain their physical-completion tracking after logical
|
|
141
|
+
cancellation. `on_complete` notifications still run, and suppressed framework
|
|
142
|
+
continuations settle as cancelled rather than remaining pending.
|
|
143
|
+
|
|
144
|
+
Execution waits only for registered JOBs' returned final results. If a JOB
|
|
145
|
+
starts X and Y but returns Y, the app is responsible for X. Normal completion
|
|
146
|
+
does not discover, join, or sweep-cancel X. If the app explicitly supplied this
|
|
147
|
+
scope to X, its API's cancellation and closed-scope start restrictions still
|
|
148
|
+
apply. A normally closed context cannot be reused to start another operation.
|
|
149
|
+
|
|
150
|
+
## Observing work started elsewhere
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Source started under its original owner and controls.
|
|
154
|
+
shared_result = existing_agent.invoke_async(question)
|
|
155
|
+
|
|
156
|
+
run = Phronomy::Execution.run_async([shared_result], timeout: 5) do |source, execution|
|
|
157
|
+
execution.observe(source).map { |response| response.fetch(:output).upcase }
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`observe` returns a distinct scoped result. It does not restart, rebind or cancel
|
|
162
|
+
the source, its existing continuations, or its physical work. A JOB may also
|
|
163
|
+
return an external result directly if it only needs to wait for it.
|
|
164
|
+
|
|
165
|
+
`TaskResult.all_settled(results)` is the public wait-only API. It accepts an Array
|
|
166
|
+
of TaskResult instances/subclasses, snapshots the positions, and preserves order
|
|
167
|
+
and duplicate positions. Invalid elements cause an immediate TypeError before
|
|
168
|
+
any source is subscribed. It accepts an empty list and has no timeout/cancel
|
|
169
|
+
parameters. It does not own or infer scopes from its sources. If subsequent
|
|
170
|
+
transformations should be scoped, explicitly use
|
|
171
|
+
`execution.observe(TaskResult.all_settled(results)).map { ... }`.
|
|
172
|
+
|
|
173
|
+
## Whole-result processing and existing layers
|
|
174
|
+
|
|
175
|
+
The whole TaskResult is the scope exit. An outer `map`/`flat_map` does not inherit
|
|
176
|
+
or extend the completed inner scope, whether registered before or after fan-in.
|
|
177
|
+
For example, vote counting belongs in an outer `map`. A slow save belongs in an
|
|
178
|
+
outer `flat_map` returning `Blocking.call_async(timeout: 5) { ... }`. That new
|
|
179
|
+
operation's five seconds start when it is invoked, independently of the first
|
|
180
|
+
execution's deadline. There is no `compose` argument or separate fan-out entrance.
|
|
181
|
+
|
|
182
|
+
Execution owns common runtime coordination. Orchestrator remains above the Agent
|
|
183
|
+
layer: `dispatch_parallel[_async]` retains Agent construction, knowledge
|
|
184
|
+
inheritance, bounded active children and `on_error` policy, and delegates its
|
|
185
|
+
runtime coordination to Execution. AgentExecution remains the durable Agent
|
|
186
|
+
record; ExecutionCoordinator keeps admission, persistence, approval, recovery
|
|
187
|
+
and terminal barriers while reusing the common context/control binding.
|
|
188
|
+
|
|
189
|
+
## Development-release migration
|
|
190
|
+
|
|
191
|
+
- Replace `Phronomy::Task` with `Phronomy::TaskResult`. The old constant and file
|
|
192
|
+
are removed; there is no compatibility alias.
|
|
193
|
+
- Replace `orchestrator.fan_out(agent: klass, inputs: inputs, ...)` with
|
|
194
|
+
`orchestrator.dispatch_parallel(*inputs.map { |input| {agent: klass, input: input} }, ...)`
|
|
195
|
+
when keeping Orchestrator's Agent construction/knowledge/concurrency policy.
|
|
196
|
+
Use Execution directly for application-defined JOB result composition.
|
|
197
|
+
- Replace the asynchronous equivalent with `dispatch_parallel_async` or
|
|
198
|
+
`Execution.run_async` as appropriate. `fan_out` and `fan_out_async` are removed.
|
|
199
|
+
- Keep per-incarnation listeners at Agent creation/load. This change introduces
|
|
200
|
+
no invocation listener blocks, Proc persistence, durable result transforms,
|
|
201
|
+
callback acknowledgements or new scheduler.
|
|
202
|
+
|
|
203
|
+
See examples `32_async_composition` for complete basic and asynchronously
|
|
204
|
+
evaluated majority-vote applications, and `23_bounded_parallel` for bounded
|
|
205
|
+
Agent dispatch. Examples must depend on a core commit containing these APIs.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Accepted — revised for a single
|
|
5
|
+
Accepted — revised for a single TaskResult completion contract and the OffloadPool
|
|
6
6
|
execution boundary.
|
|
7
7
|
|
|
8
8
|
## Context
|
|
@@ -19,7 +19,7 @@ executes is separate from the object used by callers to observe completion.
|
|
|
19
19
|
## Decision
|
|
20
20
|
|
|
21
21
|
Framework lifecycle coordination uses one Runtime-owned EventLoop and explicit
|
|
22
|
-
FSMSession state/events. **`Phronomy::
|
|
22
|
+
FSMSession state/events. **`Phronomy::TaskResult` is the single caller-facing completion
|
|
23
23
|
handle**, not an execution backend.
|
|
24
24
|
|
|
25
25
|
There are two framework execution mechanisms:
|
|
@@ -28,22 +28,22 @@ There are two framework execution mechanisms:
|
|
|
28
28
|
- **OffloadPool** for synchronous work that must execute on bounded worker OS
|
|
29
29
|
Threads away from EventLoop.
|
|
30
30
|
|
|
31
|
-
Both mechanisms surface asynchronous completion as `Phronomy::
|
|
31
|
+
Both mechanisms surface asynchronous completion as `Phronomy::TaskResult`.
|
|
32
32
|
|
|
33
33
|
Phronomy defines two Tool execution modes:
|
|
34
34
|
|
|
35
35
|
- `:cooperative` — short, EventLoop-safe work, or a specialized asynchronous
|
|
36
36
|
implementation that starts another Phronomy lifecycle and immediately returns
|
|
37
|
-
a
|
|
37
|
+
a TaskResult;
|
|
38
38
|
- `:offloaded` — synchronous work that must not run on EventLoop. It executes in
|
|
39
|
-
the bounded OffloadPool and returns a
|
|
39
|
+
the bounded OffloadPool and returns a TaskResult.
|
|
40
40
|
|
|
41
41
|
Workload classification such as I/O-bound versus CPU-bound is application-owned.
|
|
42
42
|
Phronomy does not provide separate `:blocking_io`, `:cpu_bound`, or
|
|
43
43
|
`:external_process` Tool execution modes.
|
|
44
44
|
|
|
45
45
|
Logical waits are never offloaded merely to obtain concurrency. Waiting for an
|
|
46
|
-
Agent, Workflow, ToolInvocation, approval, timer, or another
|
|
46
|
+
Agent, Workflow, ToolInvocation, approval, timer, or another TaskResult is represented
|
|
47
47
|
as FSMSession state plus a later EventLoop event.
|
|
48
48
|
|
|
49
49
|
## Runtime model
|
|
@@ -65,7 +65,7 @@ Runtime
|
|
|
65
65
|
└─ EventLoop-driven timers
|
|
66
66
|
|
|
67
67
|
EventLoop / FSMSession ─┐
|
|
68
|
-
├─>
|
|
68
|
+
├─> TaskResult = completion handle
|
|
69
69
|
OffloadPool ────────────┘
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -91,11 +91,11 @@ resource domains.
|
|
|
91
91
|
An EventLoop action must not block while waiting for a free OffloadPool queue
|
|
92
92
|
slot. Framework-owned EventLoop-origin submissions therefore use non-blocking
|
|
93
93
|
admission (`on_full: :raise`) and propagate `BackpressureError` through the normal
|
|
94
|
-
FSM/
|
|
94
|
+
FSM/TaskResult completion path.
|
|
95
95
|
|
|
96
96
|
## Timeout and cancellation
|
|
97
97
|
|
|
98
|
-
An OffloadPool submit-time timeout settles the caller-facing
|
|
98
|
+
An OffloadPool submit-time timeout settles the caller-facing TaskResult with
|
|
99
99
|
`TimeoutError`. It does not asynchronously interrupt a running worker Thread. If
|
|
100
100
|
execution has already started, the private Operation becomes abandoned, the
|
|
101
101
|
worker may continue until the submitted synchronous call returns, and that
|
|
@@ -104,8 +104,8 @@ eventual worker result is discarded.
|
|
|
104
104
|
The cancellation token passed to `OffloadPool#submit` follows the same model:
|
|
105
105
|
|
|
106
106
|
- cancellation before worker execution prevents the submitted block from
|
|
107
|
-
starting and settles the
|
|
108
|
-
- cancellation after worker execution starts settles the
|
|
107
|
+
starting and settles the TaskResult as cancelled;
|
|
108
|
+
- cancellation after worker execution starts settles the TaskResult immediately, marks
|
|
109
109
|
the private Operation abandoned, and allows the worker to continue;
|
|
110
110
|
- cancellation does not use `Thread#raise`;
|
|
111
111
|
- application code may observe the same CancellationToken and terminate its own
|
|
@@ -114,18 +114,18 @@ The cancellation token passed to `OffloadPool#submit` follows the same model:
|
|
|
114
114
|
A submit token with a monotonic deadline is connected to the Runtime timer queue,
|
|
115
115
|
so deadline expiry becomes explicit cancellation without a polling Thread.
|
|
116
116
|
|
|
117
|
-
`
|
|
117
|
+
`TaskResult#wait_result(timeout:)` is a synchronous bridge for non-EventLoop callers.
|
|
118
118
|
Its timeout is waiter-local: it raises `TimeoutError` only to that caller and does
|
|
119
|
-
not settle the
|
|
119
|
+
not settle the TaskResult, cancel the submitted operation, or mark an OffloadPool
|
|
120
120
|
Operation abandoned.
|
|
121
121
|
|
|
122
|
-
Framework components own
|
|
122
|
+
Framework components own TaskResult settlement. Application code should request
|
|
123
123
|
operation-wide cancellation through the CancellationToken accepted by the API
|
|
124
|
-
that created the
|
|
125
|
-
|
|
124
|
+
that created the TaskResult rather than calling TaskResult settlement methods directly. A
|
|
125
|
+
TaskResult cancellation must not implicitly cancel a shared CancellationToken in the
|
|
126
126
|
reverse direction.
|
|
127
127
|
|
|
128
|
-
Independent `
|
|
128
|
+
Independent `TaskResult#on_complete` notification callbacks are fault-isolated. Their
|
|
129
129
|
execution thread is not guaranteed, so callbacks must be thread-safe and should
|
|
130
130
|
return quickly. Framework lifecycle code normally converts completion into an
|
|
131
131
|
explicit EventLoop event.
|
|
@@ -136,7 +136,7 @@ explicit EventLoop event.
|
|
|
136
136
|
caller-facing timeout or cancellation won after worker execution had already
|
|
137
137
|
started. `offload_pool_abandoned_active` is current-state: it counts only those
|
|
138
138
|
abandoned operations whose synchronous workers are still occupying OffloadPool
|
|
139
|
-
capacity.
|
|
139
|
+
capacity. TaskResult does not expose abandonment as caller-facing completion state;
|
|
140
140
|
that distinction remains private OffloadPool execution/observability state.
|
|
141
141
|
|
|
142
142
|
## CPU-bound work
|
|
@@ -154,7 +154,7 @@ does not reintroduce a Tool-level workload class.
|
|
|
154
154
|
A component that truly uses a native asynchronous driver and does not create a
|
|
155
155
|
Phronomy-owned OS Thread does not need an OffloadPool worker. If such an
|
|
156
156
|
extension point is formally introduced, it must still adapt completion into a
|
|
157
|
-
`Phronomy::
|
|
157
|
+
`Phronomy::TaskResult`; it must not expose provider-specific futures or private Runtime
|
|
158
158
|
operation records as Phronomy's completion contract.
|
|
159
159
|
|
|
160
160
|
The current VectorStore and Embeddings extension contracts are synchronous; their
|
|
@@ -168,14 +168,14 @@ OffloadPool worker
|
|
|
168
168
|
→ wait_result
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
and equivalently for Workflow/ToolInvocation/
|
|
171
|
+
and equivalently for Workflow/ToolInvocation/TaskResult lifecycles.
|
|
172
172
|
|
|
173
173
|
The correct model is:
|
|
174
174
|
|
|
175
175
|
```text
|
|
176
176
|
parent FSMSession
|
|
177
177
|
→ start child lifecycle
|
|
178
|
-
→ return
|
|
178
|
+
→ return TaskResult immediately
|
|
179
179
|
→ child settles
|
|
180
180
|
→ post parent EventLoop event
|
|
181
181
|
```
|
|
@@ -183,8 +183,8 @@ parent FSMSession
|
|
|
183
183
|
## Consequences
|
|
184
184
|
|
|
185
185
|
- There is one explicit framework continuation model: FSMSession + EventLoop.
|
|
186
|
-
- There is one caller-facing completion abstraction:
|
|
187
|
-
-
|
|
186
|
+
- There is one caller-facing completion abstraction: TaskResult.
|
|
187
|
+
- TaskResult stays thread-free and represents settlement only.
|
|
188
188
|
- OffloadPool owns bounded OS-thread execution and its private Operation state.
|
|
189
189
|
- Tool execution classification remains `:cooperative` / `:offloaded`.
|
|
190
190
|
- CPU/I/O classification and resource sizing are application responsibilities.
|
data/docs/features.md
CHANGED
|
@@ -46,8 +46,8 @@ rather than implicitly inheriting the parent revision. The Stable
|
|
|
46
46
|
|---|---|
|
|
47
47
|
| **Knowledge** — Journal-backed persistent Agent context registered with `knowledge:` / `add_knowledge`, selected per LLM call by Context Policy | Beta |
|
|
48
48
|
| **`VectorStore#size`** — Document count for InMemory, RedisSearch, and Pgvector backends | Beta |
|
|
49
|
-
| **VectorStore async convenience** — `add_async` / `search_async` / `remove_async` / `clear_async` offload the synchronous Backend SPI through Phronomy and return `
|
|
50
|
-
| **Embedding async convenience** — `embed_async` offloads synchronous `embed` through Phronomy and returns `
|
|
49
|
+
| **VectorStore async convenience** — `add_async` / `search_async` / `remove_async` / `clear_async` offload the synchronous Backend SPI through Phronomy and return `TaskResult`; native async override is not part of the current Backend SPI | Beta |
|
|
50
|
+
| **Embedding async convenience** — `embed_async` offloads synchronous `embed` through Phronomy and returns `TaskResult` | Beta |
|
|
51
51
|
| **Model Context Protocol (MCP) Tool** — `Phronomy::Tools::Mcp` integration through the official `mcp` gem | Beta |
|
|
52
52
|
| **Agent Tool** — `Phronomy::Tools::Agent.from_agent` exposes a child Agent as a Tool without occupying a worker while waiting | Beta |
|
|
53
53
|
| **Vector Search Tool** — `Phronomy::Tools::VectorSearch` wraps VectorStore and Embeddings adapters | Beta |
|
|
@@ -64,10 +64,12 @@ rather than implicitly inheriting the parent revision. The Stable
|
|
|
64
64
|
| **Agent async events** — one Runtime-only `on_event` listener is bound at Agent `new` / `create` / `load`; invoke/stream operations publish through that listener and streaming additionally emits `:token` | Beta |
|
|
65
65
|
| **`stream` / `stream_async`** — Event callbacks execute on EventLoop and must return quickly | Beta |
|
|
66
66
|
| **`stream_callback_error_policy`** — Terminal event callback error policy (`:report` / `:fail_task`) | Beta |
|
|
67
|
-
| **
|
|
68
|
-
| **`
|
|
69
|
-
|
|
|
70
|
-
|
|
|
67
|
+
| **TaskResult completion contract** — `TaskResult` is the common caller-facing completion handle for EventLoop/FSMSession lifecycles and OffloadPool work | Beta |
|
|
68
|
+
| **`TaskResult#map`** — Application-level TaskResult result transformation and error propagation | Stable |
|
|
69
|
+
| **`TaskResult#flat_map` / `.all_settled`** — Asynchronous chaining and input-order observation of terminal results | Beta |
|
|
70
|
+
| **`Execution.run_async` / `.run`** — Common runtime fan-out/fan-in with whole-execution controls and immutable outcome snapshots | Beta |
|
|
71
|
+
| **Settled TaskResult factories** — Public `TaskResult.completed` / `TaskResult.failed` represent already available application results without starting execution | Beta |
|
|
72
|
+
| **Blocking.call_async** — Public non-waiting admission of synchronous application work to the existing OffloadPool, returning a TaskResult | Beta |
|
|
71
73
|
| **CancellationToken** — Cooperative cancellation with explicit `cancel!`, lazy monotonic deadlines, and callback registration | Experimental |
|
|
72
74
|
| **Tool `execution_mode`** — `:cooperative` for short EventLoop-safe work; `:offloaded` for synchronous work that must stay off EventLoop | Experimental |
|
|
73
75
|
| **OffloadPool sizing** — `offload_pool_size` / `offload_queue_size`; named pools available for application-owned isolation | Beta |
|
|
@@ -107,11 +109,11 @@ visibility still follows the intended calling model. `@api private` means
|
|
|
107
109
|
declaration; some internal methods remain Ruby-public because Phronomy components
|
|
108
110
|
call them through explicit receivers.
|
|
109
111
|
|
|
110
|
-
`
|
|
112
|
+
`TaskResult` is the caller-facing completion abstraction. Framework components own
|
|
111
113
|
settlement (`complete` / `fail` / `cancel!`); application code observes Tasks via
|
|
112
114
|
`wait_result`, `on_complete`, `map`, and state readers. Operation-wide cancellation
|
|
113
115
|
is requested through the `CancellationToken` accepted by the API that created the
|
|
114
|
-
|
|
116
|
+
TaskResult.
|
|
115
117
|
|
|
116
118
|
Persistence Backend SPI methods, LLMAdapter methods, and other documented
|
|
117
119
|
extension contracts are deliberate exceptions to the ordinary
|
|
@@ -147,7 +149,8 @@ private execution machinery, see [Runtime and concurrency](runtime-and-concurren
|
|
|
147
149
|
|
|
148
150
|
Static `Orchestrator.subagent` Tools invoked inside a parent AgentExecution reserve
|
|
149
151
|
child Agent/execution identities before dispatch and reuse retained outcomes.
|
|
150
|
-
Direct `dispatch_parallel`
|
|
152
|
+
Direct `dispatch_parallel` calls retain Agent-specific policy and use the common
|
|
153
|
+
runtime Execution engine. The redundant `fan_out` / `fan_out_async` APIs are removed.
|
|
151
154
|
`Orchestrator#resume(execution_id)` continues retained parent coordination.
|
|
152
155
|
|
|
153
156
|
`TeamCoordinator` requires `team_definition id:, version:` and provides
|
data/docs/getting-started.md
CHANGED
|
@@ -180,13 +180,13 @@ Supplying both `on_event:` and a construction block is an error. If `load`
|
|
|
180
180
|
resolves an already-live same-process Agent, supplying any new listener/block
|
|
181
181
|
is also an error; the existing binding is immutable for that Runtime incarnation.
|
|
182
182
|
|
|
183
|
-
`Phronomy::
|
|
183
|
+
`Phronomy::TaskResult` is the common caller-facing completion handle for asynchronous
|
|
184
184
|
Phronomy work. Logical lifecycle progress is driven by EventLoop/FSMSession;
|
|
185
185
|
synchronous work that must execute away from EventLoop is submitted to
|
|
186
|
-
OffloadPool. Both paths expose completion as a `
|
|
186
|
+
OffloadPool. Both paths expose completion as a `TaskResult`.
|
|
187
187
|
|
|
188
|
-
`
|
|
189
|
-
a
|
|
188
|
+
`TaskResult#wait_result` is for an external caller. Do not block EventLoop waiting for
|
|
189
|
+
a TaskResult that can only complete through that same EventLoop.
|
|
190
190
|
|
|
191
191
|
Streaming uses the same Agent-incarnation listener:
|
|
192
192
|
|
|
@@ -208,7 +208,7 @@ EventLoop and therefore should return quickly.
|
|
|
208
208
|
## Human-in-the-loop approval
|
|
209
209
|
|
|
210
210
|
A Tool requiring approval suspends the durable logical execution without
|
|
211
|
-
settling the original execution
|
|
211
|
+
settling the original execution TaskResult. Approval notification is delivered
|
|
212
212
|
through the same Agent listener as `:approval_required`:
|
|
213
213
|
|
|
214
214
|
```ruby
|
|
@@ -349,7 +349,7 @@ workflow = Phronomy::Workflow.define(AnswerContext) do
|
|
|
349
349
|
end
|
|
350
350
|
```
|
|
351
351
|
|
|
352
|
-
Returning a `Phronomy::
|
|
352
|
+
Returning a `Phronomy::TaskResult` from a Workflow entry/transition action is not an
|
|
353
353
|
implicit await mechanism and is rejected.
|
|
354
354
|
|
|
355
355
|
## Agent as Tool
|