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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +10 -7
  5. data/benchmark/bench_regression.rb +1 -1
  6. data/docs/architecture/agent-context.md +1 -1
  7. data/docs/architecture/persistence.md +2 -2
  8. data/docs/architecture/tracing.md +3 -3
  9. data/docs/async-composition.md +205 -0
  10. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  11. data/docs/features.md +12 -9
  12. data/docs/getting-started.md +6 -6
  13. data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
  14. data/docs/persistence-backends.md +1 -1
  15. data/docs/runtime-and-concurrency.md +89 -29
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
  17. data/lib/phronomy/agent/base.rb +4 -10
  18. data/lib/phronomy/agent/exact_execution.rb +3 -3
  19. data/lib/phronomy/agent/execution_coordinator.rb +20 -7
  20. data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
  21. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
  22. data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
  23. data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
  24. data/lib/phronomy/agent/tool_executor.rb +4 -4
  25. data/lib/phronomy/agent/tool_invocation.rb +1 -1
  26. data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
  27. data/lib/phronomy/blocking.rb +15 -8
  28. data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
  29. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  30. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  31. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  32. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  33. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  34. data/lib/phronomy/engine/event_loop.rb +7 -7
  35. data/lib/phronomy/engine/fsm_session.rb +3 -3
  36. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  37. data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
  38. data/lib/phronomy/execution.rb +236 -0
  39. data/lib/phronomy/execution_cancellation_error.rb +12 -0
  40. data/lib/phronomy/execution_timeout_error.rb +12 -0
  41. data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
  42. data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
  43. data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
  44. data/lib/phronomy/invocation_context.rb +13 -1
  45. data/lib/phronomy/llm_adapter/base.rb +2 -2
  46. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
  47. data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
  48. data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
  49. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  50. data/lib/phronomy/testing/fake_clock.rb +13 -9
  51. data/lib/phronomy/tools/agent.rb +5 -5
  52. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  53. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  54. data/lib/phronomy/version.rb +1 -1
  55. data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
  56. data/lib/phronomy/workflow.rb +2 -2
  57. data/lib/phronomy/workflow_runner.rb +4 -4
  58. data/scripts/api_snapshot.rb +3 -1
  59. data/sig/phronomy/agent.rbs +4 -4
  60. data/sig/phronomy/extensions.rbs +5 -5
  61. data/sig/phronomy/multi_agent.rbs +1 -3
  62. data/sig/phronomy/runtime.rbs +34 -7
  63. data/sig/phronomy/tool.rbs +1 -1
  64. data/sig/phronomy/workflow.rbs +1 -1
  65. metadata +11 -5
  66. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  67. 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: 636ce578204172927316a89f0f65a2bdfda49140822da98cd1eec9230d821105
4
- data.tar.gz: 8d776e0ab9bf2775af4e23f77b0b29968a3d4962f8fbd877d3fe9574a3efb306
3
+ metadata.gz: dd6beeb52633381ff9e249ec18a1dc422234c78e44f479e3549cd3c54591ab04
4
+ data.tar.gz: 6528144759933b359d2303b1499b678cbd4eb0c747ca9966f0c6542effb28196
5
5
  SHA512:
6
- metadata.gz: 81a2b295d5fb03d4cd03c6f3ceabca7b62f4e635fc5dd6ed3db31b1c5f59c6031fc38958895a1fc7b8e9fc75394a81181d51529ebb86f310e85cd2300a9395af
7
- data.tar.gz: f701f8fd0be60d655e9676c8cbdfec93606cca20769da90b7c67617c9d876cbde51bf18a42531b7cf70079aa12863af022acd21500fe40cff64c04d5a70df6ab
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::Task` is the caller-facing completion handle. EventLoop/FSMSession and
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 Task later.
108
+ lifecycles stay on EventLoop/FSMSession and settle a TaskResult later.
109
109
 
110
- The framework owns Task settlement (`complete`, `fail`, and framework-driven
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, Task, and OffloadPool execution boundary. ADR-015 defines
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
- - **Task** — the common thread-free completion handle returned by Phronomy asynchronous APIs, including OffloadPool-backed work.
26
- - **Task.completed / Task.failed** — already-settled application results without starting execution.
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::Task`. Inside Phronomy lifecycle callbacks, do not block waiting for
90
- another Task; continue through explicit events instead.
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
- ├─> Task = completion handle
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::Task`. See
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, Task, OffloadPool, cancellation, observability.
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::Task.deferred(name: "bench-stub")
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 Task callbacks. It
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
- Task instances/callbacks, EventLoop entries, Runtime admission entries, and
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 Task
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 Task settles.
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 Task settlement.
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
- Task/EventLoop/Offload/Runtime boundaries.
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 Task completion contract and the OffloadPool
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::Task` is the single caller-facing completion
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::Task`.
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 Task;
37
+ a TaskResult;
38
38
  - `:offloaded` — synchronous work that must not run on EventLoop. It executes in
39
- the bounded OffloadPool and returns a Task.
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 Task is represented
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
- ├─> Task = completion handle
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/Task completion path.
94
+ FSM/TaskResult completion path.
95
95
 
96
96
  ## Timeout and cancellation
97
97
 
98
- An OffloadPool submit-time timeout settles the caller-facing Task with
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 Task as cancelled;
108
- - cancellation after worker execution starts settles the Task immediately, marks
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
- `Task#wait_result(timeout:)` is a synchronous bridge for non-EventLoop callers.
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 Task, cancel the submitted operation, or mark an OffloadPool
119
+ not settle the TaskResult, cancel the submitted operation, or mark an OffloadPool
120
120
  Operation abandoned.
121
121
 
122
- Framework components own Task settlement. Application code should request
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 Task rather than calling Task settlement methods directly. A
125
- Task cancellation must not implicitly cancel a shared CancellationToken in the
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 `Task#on_complete` notification callbacks are fault-isolated. Their
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. Task does not expose abandonment as caller-facing completion state;
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::Task`; it must not expose provider-specific futures or private Runtime
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/Task lifecycles.
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 Task immediately
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: Task.
187
- - Task stays thread-free and represents settlement only.
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 `Task`; 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 `Task` | Beta |
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
- | **Task completion contract** — `Task` is the common caller-facing completion handle for EventLoop/FSMSession lifecycles and OffloadPool work | Beta |
68
- | **`Task#map`** — Application-level Task result transformation and error propagation | Stable |
69
- | **Settled Task factories**Public `Task.completed` / `Task.failed` represent already available application results without starting execution | Beta |
70
- | **Blocking.call_async** Public non-waiting admission of synchronous application work to the existing OffloadPool, returning a Task | Beta |
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
- `Task` is the caller-facing completion abstraction. Framework components own
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
- Task.
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` / `fan_out` calls remain Runtime-only convenience APIs.
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
@@ -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::Task` is the common caller-facing completion handle for asynchronous
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 `Task`.
186
+ OffloadPool. Both paths expose completion as a `TaskResult`.
187
187
 
188
- `Task#wait_result` is for an external caller. Do not block EventLoop waiting for
189
- a Task that can only complete through that same EventLoop.
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 Task. Approval notification is delivered
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::Task` from a Workflow entry/transition action is not an
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