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
@@ -42,7 +42,7 @@ results without hydration/callbacks. `Persistence#handoff_result(source_id)`
42
42
  follows the exact transfer chain without constructing a graph or Agent owners. `Orchestrator#resume(id)` continues existing
43
43
  static subagent coordination. There is no global generic class registry.
44
44
 
45
- Observer Task wait timeouts and shutdown do not request cancellation. Team's
45
+ Observer TaskResult wait timeouts and shutdown do not request cancellation. Team's
46
46
  `cancel(run_id)` persists a run-scoped request before forwarding its live token;
47
47
  resume settles exact children or returns the existing rehydration error. Handoff's
48
48
  `cancel(execution_id)` follows only that turn. A cancellation token passed to
@@ -32,7 +32,7 @@ the following as part of this SPI:
32
32
  - EventLoop Agent execution-directory entries;
33
33
  - `AgentInvocation`;
34
34
  - `FSMSession`;
35
- - `Task` or callbacks;
35
+ - `TaskResult` or callbacks;
36
36
  - EventLoop queue contents;
37
37
  - Runtime Workflow admission entries;
38
38
  - in-flight provider operations.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Phronomy uses an **EventLoop / FSMSession first** architecture for framework
4
4
  lifecycle coordination. `FSMSession` represents explicit lifecycle state and
5
- events. `Phronomy::Task` is the common caller-facing completion handle, not an
5
+ events. `Phronomy::TaskResult` is the common caller-facing completion handle, not an
6
6
  execution backend. Synchronous work that must stay off EventLoop is isolated in
7
7
  the bounded `OffloadPool`.
8
8
 
@@ -46,7 +46,7 @@ Runtime
46
46
  └─ EventLoop-driven timers
47
47
 
48
48
  EventLoop / FSMSession ─┐
49
- ├─> Task = completion handle
49
+ ├─> TaskResult = completion handle
50
50
  OffloadPool ────────────┘
51
51
  ```
52
52
 
@@ -254,7 +254,7 @@ lifecycle. Logical halt/completion first enters a private
254
254
  `persisting_terminal` lifecycle condition; the FSMSession remains nonterminal
255
255
  while WorkflowRunner saves the terminal snapshot through OffloadPool. Only a
256
256
  known-successful save result returned to that same FSMSession permits
257
- `HALTED`/`COMPLETED`, admission release, and caller Task settlement.
257
+ `HALTED`/`COMPLETED`, admission release, and caller TaskResult settlement.
258
258
 
259
259
  ```text
260
260
  RUNNING
@@ -282,10 +282,10 @@ remain durable conflict defense rather than distributed ownership.
282
282
  Phronomy exposes two execution modes for capabilities:
283
283
 
284
284
  - `:cooperative` — short EventLoop-safe work, or specialized asynchronous work
285
- that starts another Phronomy lifecycle and returns a Task immediately;
285
+ that starts another Phronomy lifecycle and returns a TaskResult immediately;
286
286
  - `:offloaded` — synchronous work that must not run to completion on EventLoop.
287
287
 
288
- Both paths return `Phronomy::Task`. The execution mechanism differs; the
288
+ Both paths return `Phronomy::TaskResult`. The execution mechanism differs; the
289
289
  completion abstraction does not.
290
290
 
291
291
  Phronomy does not classify application work into framework-level I/O/CPU/process
@@ -310,7 +310,7 @@ Correct shape:
310
310
  parent FSMSession
311
311
  → start child lifecycle
312
312
  → return immediately
313
- → child Task settles
313
+ → child TaskResult settles
314
314
  → post parent EventLoop event
315
315
  ```
316
316
 
@@ -326,7 +326,7 @@ Agent preparation/commit and Workflow hydrate/save operations are submitted to
326
326
 
327
327
  A durable barrier may pause one logical lifecycle without blocking EventLoop.
328
328
  Persistence does not implement async repository variants and must not depend on
329
- EventLoop, FSMSession, Task settlement internals, or private OffloadPool operation
329
+ EventLoop, FSMSession, TaskResult settlement internals, or private OffloadPool operation
330
330
  records.
331
331
 
332
332
  ## Sync versus async application APIs
@@ -336,7 +336,7 @@ records.
336
336
  | Top-level application code | `agent.invoke(...)` when blocking the caller is acceptable |
337
337
  | Top-level explicit async | `task = agent.invoke_async(...)`; optionally `task.wait_result` outside EventLoop |
338
338
  | Workflow entry/transition action | Start async work and continue through `Workflow#signal` |
339
- | EventLoop callback | Never block waiting for a Task that requires EventLoop progress |
339
+ | EventLoop callback | Never block waiting for a TaskResult that requires EventLoop progress |
340
340
  | Top-level streaming | `agent.stream(...)` |
341
341
  | Non-blocking streaming | `agent.stream_async(...)` |
342
342
  | Approval from EventLoop callback | Resolve with `live_for_execution`, call `agent.approve_async(...)`, and return immediately |
@@ -345,37 +345,88 @@ Blocking synchronous APIs reject EventLoop re-entry with
345
345
  `Phronomy::EventLoopReentrancyError` when waiting would stall the same EventLoop
346
346
  needed for progress.
347
347
 
348
- ## Task
348
+ ## TaskResult
349
349
 
350
- `Phronomy::Task` is thread-free. It represents one terminal result:
350
+ `Phronomy::TaskResult` is thread-free. It represents one terminal result:
351
351
 
352
352
  - completed value;
353
353
  - failure;
354
354
  - cancellation.
355
355
 
356
- Task is the common completion abstraction for logical EventLoop/FSMSession
356
+ TaskResult is the common completion abstraction for logical EventLoop/FSMSession
357
357
  lifecycles and OffloadPool-backed synchronous work.
358
358
 
359
- `Task#wait_result(timeout:)` is a bridge for external synchronous callers. Its
360
- timeout is waiter-local: it does not settle/cancel the Task or alter OffloadPool
359
+ `TaskResult#wait_result(timeout:)` is a bridge for external synchronous callers. Its
360
+ timeout is waiter-local: it does not settle/cancel the TaskResult or alter OffloadPool
361
361
  abandonment state.
362
362
 
363
- `Task#on_complete` registers an independent notification callback. Callback
363
+ `TaskResult#on_complete` registers an independent notification callback. Callback
364
364
  execution thread is not guaranteed. A callback may be delivered by an OffloadPool
365
365
  worker, a timer/cancellation caller, an EventLoop-related control path, or the
366
366
  thread that registers after settlement. Callbacks must therefore be thread-safe
367
367
  and should complete quickly. Framework lifecycle code normally converts worker
368
368
  completion into an explicit EventLoop event before applying live state.
369
369
 
370
- `Task#map` is application-level composition. A transformation exception settles
371
- the mapped Task as failed.
370
+ `TaskResult#map` is application-level composition. A transformation exception settles
371
+ the mapped TaskResult as failed.
372
372
 
373
- Framework components own Task settlement. Application code should not use
374
- `Task#complete`, `Task#fail`, or `Task#cancel!` as operation-control APIs. Request
373
+ `flat_map` connects a returned TaskResult's completion, while `all_settled`
374
+ observes an input-order list of results. `Execution.run_async` starts and joins
375
+ JOBs under whole-execution controls. See [Result composition and Execution](async-composition.md)
376
+ for the complete contracts, ownership boundaries and development-release migration.
377
+
378
+ Framework components own TaskResult settlement. Application code should not use
379
+ `TaskResult#complete`, `TaskResult#fail`, or `TaskResult#cancel!` as operation-control APIs. Request
375
380
  operation-wide cancellation through the `CancellationToken` accepted by the API
376
- that created the Task. Task settlement never propagates backwards to cancel a
381
+ that created the TaskResult. TaskResult settlement never propagates backwards to cancel a
377
382
  shared CancellationToken.
378
383
 
384
+ ### Settled TaskResult factories
385
+
386
+ `TaskResult.completed(value)` and `TaskResult.failed(error)` create results that are already
387
+ settled. They are useful when application code has a result or an error in hand
388
+ and needs to present it through the same `TaskResult` contract as a lifecycle-backed
389
+ completion:
390
+
391
+ ```ruby
392
+ # Already-computed value — no execution started.
393
+ task = Phronomy::TaskResult.completed("cached result")
394
+
395
+ # Known failure — no execution started.
396
+ task = Phronomy::TaskResult.failed(StandardError.new("precondition not met"))
397
+ ```
398
+
399
+ Settled results satisfy `TaskResult#wait_result`, `TaskResult#on_complete`, and `TaskResult#map` with
400
+ the same contract as a normally-completed TaskResult.
401
+
402
+ ## Blocking
403
+
404
+ `Phronomy::Blocking.call_async(&block)` submits a synchronous block to the
405
+ existing default Runtime OffloadPool and returns a `TaskResult`. It is the public
406
+ application entry point for one-off synchronous work that should not block the
407
+ caller:
408
+
409
+ ```ruby
410
+ task = Phronomy::Blocking.call_async { expensive_io_call }
411
+ result = task.wait_result
412
+ ```
413
+
414
+ The optional `invocation_context:` connects this operation to an Execution
415
+ scope or an existing invocation context. Context controls and the optional
416
+ individual `cancellation_token:` both apply, without reverse cancellation of
417
+ shared controls. The application chooses when synchronous work needs OffloadPool.
418
+
419
+ The method uses the existing bounded OffloadPool. It does not create a new thread
420
+ or scheduler. Capacity and backpressure semantics are inherited from the pool
421
+ configuration (`offload_pool_size` / `offload_queue_size`).
422
+
423
+ `Blocking.call_async` always uses non-waiting admission (`on_full: :raise`) and can
424
+ be called from within the EventLoop thread. Admission `StandardError` failures (for
425
+ example `BackpressureError` when the queue is full) are caught and returned as a
426
+ failed `TaskResult`. Accepted work retains the pool's original `TaskResult` unchanged. Do not
427
+ call `TaskResult#wait_result` on the returned `TaskResult` from within the EventLoop — waiting
428
+ for a `TaskResult` that requires EventLoop progress will deadlock.
429
+
379
430
  ## OffloadPool
380
431
 
381
432
  `OffloadPool` is a bounded worker pool for synchronous work that must not execute
@@ -391,7 +442,7 @@ Its guarantees include:
391
442
  - runtime metrics;
392
443
  - shutdown/drain behavior.
393
444
 
394
- `OffloadPool#submit` returns a `Phronomy::Task`. OffloadPool does not expose its
445
+ `OffloadPool#submit` returns a `Phronomy::TaskResult`. OffloadPool does not expose its
395
446
  execution record as a caller-facing future/promise. Its private `Operation` owns:
396
447
 
397
448
  - the submitted block;
@@ -403,27 +454,36 @@ execution record as a caller-facing future/promise. Its private `Operation` owns
403
454
  - metrics state needed by the pool.
404
455
 
405
456
  This separation keeps execution details private while allowing every asynchronous
406
- Phronomy API to expose the same Task completion contract.
457
+ Phronomy API to expose the same TaskResult completion contract.
407
458
 
408
459
  ### EventLoop queue admission
409
460
 
410
461
  Framework-owned EventLoop-origin submissions must not wait for a free worker
411
462
  queue slot. They use non-blocking admission (`on_full: :raise`) and route
412
- `BackpressureError` through the ordinary FSM/Task completion path.
463
+ `BackpressureError` through the ordinary FSM/TaskResult completion path.
464
+
465
+ Any internal `OffloadPool#submit` call that requests waiting admission
466
+ (`on_full: :wait` or `:timeout`) is **rejected before the Operation is created**
467
+ when the caller is on the EventLoop thread. The rejection raises
468
+ `Phronomy::EventLoopReentrancyError` directly; it does not create an Operation,
469
+ timer, cancellation subscription, or TaskResult. The public `Phronomy::Blocking.call_async`
470
+ always uses `on_full: :raise` and therefore does not trigger this guard; it converts
471
+ admission `StandardError` (such as `BackpressureError`) into a failed `TaskResult` at the
472
+ public-API layer.
413
473
 
414
474
  External management threads may choose a blocking admission policy when blocking
415
475
  the caller is acceptable.
416
476
 
417
477
  ## Submit timeout and cancellation
418
478
 
419
- Submit-time timeout and submit cancellation settle the caller-facing Task. They
479
+ Submit-time timeout and submit cancellation settle the caller-facing TaskResult. They
420
480
  do **not** asynchronously interrupt an already-running synchronous worker.
421
481
 
422
482
  ### Before worker start
423
483
 
424
484
  If timeout/cancellation wins before execution starts:
425
485
 
426
- - the Task settles (`TimeoutError` failure or cancellation);
486
+ - the TaskResult settles (`TimeoutError` failure or cancellation);
427
487
  - the submitted block does not run;
428
488
  - the private Operation is not counted as abandoned.
429
489
 
@@ -431,7 +491,7 @@ If timeout/cancellation wins before execution starts:
431
491
 
432
492
  If timeout/cancellation wins after execution starts:
433
493
 
434
- - the Task settles immediately;
494
+ - the TaskResult settles immediately;
435
495
  - the private Operation is marked abandoned;
436
496
  - the worker continues until its synchronous call returns;
437
497
  - the eventual worker result is discarded.
@@ -447,7 +507,7 @@ requiring callback delivery promote the deadline to explicit `cancel!` through
447
507
  the Runtime timer queue. OffloadPool does this for its submit cancellation token.
448
508
 
449
509
  A CancellationToken may be shared by multiple operations. For that reason,
450
- settling or cancelling one Task does not cancel the token in the reverse
510
+ settling or cancelling one TaskResult does not cancel the token in the reverse
451
511
  direction.
452
512
 
453
513
  ## Native async boundary
@@ -455,7 +515,7 @@ direction.
455
515
  A genuine native-async driver that does not create a Phronomy-owned OS Thread and
456
516
  does not block EventLoop need not consume an OffloadPool worker. If Phronomy
457
517
  formally exposes such an extension point, it must adapt completion to
458
- `Phronomy::Task` rather than exposing a provider-specific future or a private
518
+ `Phronomy::TaskResult` rather than exposing a provider-specific future or a private
459
519
  Runtime type.
460
520
 
461
521
  The current Persistence, VectorStore, Embeddings, and LLM call-extension
@@ -473,8 +533,8 @@ Two metrics answer different operational questions:
473
533
  - `offload_pool_abandoned_active` — current number of abandoned operations whose
474
534
  synchronous workers still occupy pool capacity.
475
535
 
476
- The abandonment state belongs to the private OffloadPool Operation, not to Task.
477
- Task reports only caller-facing settlement.
536
+ The abandonment state belongs to the private OffloadPool Operation, not to TaskResult.
537
+ TaskResult reports only caller-facing settlement.
478
538
 
479
539
  ## EventLoop metrics
480
540
 
@@ -542,7 +542,7 @@ module Phronomy
542
542
  end
543
543
 
544
544
  def self.register_child_session(runtime, child, session, parent_event_sink)
545
- completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
545
+ completion = Phronomy::TaskResult.deferred(name: "tool-session:#{child.id}")
546
546
  completion.on_complete do |_result, error|
547
547
  next unless error
548
548
 
@@ -799,18 +799,12 @@ module Phronomy
799
799
  def _prepare_invocation_config(config, invocation_context)
800
800
  __assert_live_agent!
801
801
  _reject_removed_generic_identity_keys!(config)
802
- effective_config = invocation_context ?
802
+ invocation_context ?
803
803
  config.merge(invocation_context: invocation_context) : config
804
804
 
805
- if invocation_context && effective_config[:cancellation_token].nil?
806
- if (tok = invocation_context.effective_timeout_token)
807
- effective_config = effective_config.merge(
808
- cancellation_token: tok,
809
- phronomy_timeout_deadline: invocation_context.deadline
810
- )
811
- end
812
- end
813
- effective_config
805
+ # ExecutionCoordinator connects context and individual controls together
806
+ # at admission, before returning the result. Do not arm a second timer
807
+ # here or replace one cancellation source with another.
814
808
  end
815
809
 
816
810
  def _reject_removed_generic_identity_keys!(config)
@@ -4,7 +4,7 @@ module Phronomy
4
4
  module Agent
5
5
  # Observes a reserved execution using ordinary Agent admission and Recovery.
6
6
  # Durable reads/materialization run off EventLoop. Workers never wait for a
7
- # child lifecycle: the existing EventLoop completion waiter settles Task.
7
+ # child lifecycle: the existing EventLoop completion waiter settles TaskResult.
8
8
  # @api private
9
9
  class ExactExecution
10
10
  Wait = Data.define(:coordinator, :agent, :execution_id)
@@ -17,7 +17,7 @@ module Phronomy
17
17
  def initialize(agent, execution_id, input, config)
18
18
  @agent, @id, @input, @config = agent, execution_id.to_s.freeze, input, config.freeze
19
19
  @runtime = Phronomy::Runtime.instance
20
- @completion = Phronomy::Task.deferred(name: "exact-execution:#{@id}")
20
+ @completion = Phronomy::TaskResult.deferred(name: "exact-execution:#{@id}")
21
21
  end
22
22
 
23
23
  def start
@@ -92,7 +92,7 @@ module Phronomy
92
92
  end
93
93
  observers = Array(state.invocation.config[:phronomy_exact_observers])
94
94
  state.invocation.merge_config!(phronomy_exact_observers: (observers + [@completion]).uniq.freeze)
95
- waiter = Phronomy::Task.deferred(name: "exact-wait:#{@id}")
95
+ waiter = Phronomy::TaskResult.deferred(name: "exact-wait:#{@id}")
96
96
  waiter.on_complete { |_result, failure| reconcile(failure) }
97
97
  @runtime.event_loop.register_agent_completion_waiter(@id, waiter)
98
98
  rescue => failure
@@ -135,7 +135,15 @@ module Phronomy
135
135
  )
136
136
  @agent.send(:__assert_live_agent!)
137
137
  @agent.send(:_reject_removed_generic_identity_keys!, config)
138
- result_task = Phronomy::Task.deferred(name: "agent-#{@agent.agent_id}-#{mode}")
138
+ result_task = Phronomy::TaskResult.deferred(name: "agent-#{@agent.agent_id}-#{mode}")
139
+ if config[:invocation_context]
140
+ binding = Phronomy::Execution.__operation_binding(
141
+ invocation_context: config[:invocation_context],
142
+ cancellation_token: config[:cancellation_token]
143
+ )
144
+ binding.bind(result_task)
145
+ config = config.merge(cancellation_token: binding.token)
146
+ end
139
147
  command = StartCommand.new(
140
148
  coordinator: self,
141
149
  input: input,
@@ -164,7 +172,7 @@ module Phronomy
164
172
  )
165
173
  @agent.send(:__assert_live_agent!)
166
174
  @agent.send(:_reject_removed_generic_identity_keys!, config)
167
- result_task = Phronomy::Task.deferred(
175
+ result_task = Phronomy::TaskResult.deferred(
168
176
  name: "agent-approval-resume:#{execution_id}"
169
177
  )
170
178
  command = ResumeCommand.new(
@@ -397,6 +405,11 @@ module Phronomy
397
405
  def begin_start_on_event_loop(request)
398
406
  runtime = Phronomy::Runtime.instance
399
407
  event_loop = runtime.event_loop
408
+ scope = request.config[:invocation_context]&.__execution_scope
409
+ if scope && (!scope.__open? || request.config[:cancellation_token]&.cancelled?)
410
+ deliver_start_failure_on_event_loop(request, scope.__cancellation_error)
411
+ return
412
+ end
400
413
  root = @agent.agent_root
401
414
  if root.lifecycle_status == :closed
402
415
  deliver_start_failure_on_event_loop(
@@ -966,7 +979,7 @@ module Phronomy
966
979
  prepared.execution.execution_id,
967
980
  request.result_task
968
981
  )
969
- source_task = Phronomy::Task.deferred(name: "#{request.result_task.name}-source")
982
+ source_task = Phronomy::TaskResult.deferred(name: "#{request.result_task.name}-source")
970
983
  source_task.on_complete do |invocation, error|
971
984
  finish_on_event_loop(
972
985
  prepared.execution.execution_id,
@@ -1927,7 +1940,7 @@ module Phronomy
1927
1940
  invocation: invocation,
1928
1941
  fsm_session_id: parent_session.id
1929
1942
  )
1930
- source_task = Phronomy::Task.deferred(name: "#{result_task.name}-source")
1943
+ source_task = Phronomy::TaskResult.deferred(name: "#{result_task.name}-source")
1931
1944
  source_task.on_complete do |completed, error|
1932
1945
  finish_on_event_loop(
1933
1946
  execution_id,
@@ -1969,7 +1982,7 @@ module Phronomy
1969
1982
  resume_event: :resume, resume_phase: :suspended, runtime: runtime)
1970
1983
  event_loop.replace_agent_execution(execution_id, invocation: invocation, fsm_session_id: session.id)
1971
1984
  event_loop.mark_agent_execution_admission(@agent.agent_id, execution_id: execution_id, state: :executing)
1972
- source = Phronomy::Task.deferred(name: "framework-tool-recovery-source")
1985
+ source = Phronomy::TaskResult.deferred(name: "framework-tool-recovery-source")
1973
1986
  source.on_complete do |result, error|
1974
1987
  finish_on_event_loop(execution_id, result_task, result || session.context, error, fsm_session_id: session.id)
1975
1988
  end
@@ -1982,7 +1995,7 @@ module Phronomy
1982
1995
  end
1983
1996
 
1984
1997
  def register_child(event_loop, child, session, parent_event_sink)
1985
- completion = Phronomy::Task.deferred(name: "tool-session:#{child.id}")
1998
+ completion = Phronomy::TaskResult.deferred(name: "tool-session:#{child.id}")
1986
1999
  completion.on_complete do |_result, error|
1987
2000
  next unless error
1988
2001
 
@@ -2170,7 +2183,7 @@ module Phronomy
2170
2183
  )
2171
2184
  task = runtime.offload.submit(on_full: :raise) do
2172
2185
  # Only the operation-specific immutable durable snapshot crosses the
2173
- # worker boundary. Task/listener delivery state stays outside it.
2186
+ # worker boundary. TaskResult/listener delivery state stays outside it.
2174
2187
  compute_terminal(operation)
2175
2188
  end
2176
2189
  task.on_complete do |outcome, error|
@@ -140,9 +140,9 @@ module Phronomy
140
140
  def build_entry_callback(callable, state_name)
141
141
  ->(machine) {
142
142
  result = callable.call(machine.context)
143
- if result.is_a?(Phronomy::Task)
143
+ if result.is_a?(Phronomy::TaskResult)
144
144
  raise Phronomy::InvalidAsyncEntryActionError,
145
- "Agent entry action for #{state_name.inspect} returned Phronomy::Task"
145
+ "Agent entry action for #{state_name.inspect} returned Phronomy::TaskResult"
146
146
  end
147
147
  machine.context = result if result.respond_to?(:set_graph_metadata)
148
148
  }
@@ -128,7 +128,7 @@ module Phronomy
128
128
  resume_event: resume_event, resume_phase: resume_phase, runtime: @runtime)
129
129
  event_loop.replace_agent_execution(execution.execution_id, invocation: invocation, fsm_session_id: session.id)
130
130
  event_loop.register_agent_completion_waiter(execution.execution_id, completion)
131
- source = Phronomy::Task.deferred(name: "#{completion.name}-source")
131
+ source = Phronomy::TaskResult.deferred(name: "#{completion.name}-source")
132
132
  source.on_complete do |completed, error|
133
133
  main.send(:finish_on_event_loop, execution.execution_id, completion,
134
134
  completed || session.context, error, fsm_session_id: session.id)
@@ -315,7 +315,7 @@ module Phronomy
315
315
  main = agent.send(:execution_coordinator_for, agent.__coordination_config)
316
316
 
317
317
  if framework_batch?(execution)
318
- internal = Phronomy::Task.deferred(name: "framework-tool-recovery:#{execution.execution_id}")
318
+ internal = Phronomy::TaskResult.deferred(name: "framework-tool-recovery:#{execution.execution_id}")
319
319
  continue_recovery_on_event_loop(execution, internal, material: material)
320
320
  completion.complete(agent)
321
321
  return
@@ -323,7 +323,7 @@ module Phronomy
323
323
 
324
324
  case execution.phase.to_sym
325
325
  when :preparing
326
- internal_task = Phronomy::Task.deferred(
326
+ internal_task = Phronomy::TaskResult.deferred(
327
327
  name: "agent-recovery-auto:#{execution.execution_id}"
328
328
  )
329
329
  observe_recovery_execution(internal_task, execution)
@@ -348,7 +348,7 @@ module Phronomy
348
348
  raise Phronomy::ExecutionRehydrationRequiredError,
349
349
  "approved resuming execution requires Tool outcome resolution"
350
350
  end
351
- internal_task = Phronomy::Task.deferred(
351
+ internal_task = Phronomy::TaskResult.deferred(
352
352
  name: "agent-recovery-auto:#{execution.execution_id}"
353
353
  )
354
354
  observe_recovery_execution(internal_task, execution)
@@ -366,7 +366,7 @@ module Phronomy
366
366
  )
367
367
  completion.complete(agent)
368
368
  when :recovery_tools_completed, :recovery_provider_completed, :recovery_resolved_failed
369
- internal_task = Phronomy::Task.deferred(name: "agent-recovery-auto:#{execution.execution_id}")
369
+ internal_task = Phronomy::TaskResult.deferred(name: "agent-recovery-auto:#{execution.execution_id}")
370
370
  continue_recovery_on_event_loop(execution, internal_task, material: material)
371
371
  completion.complete(agent)
372
372
  else
@@ -89,7 +89,7 @@ module Phronomy
89
89
  "load must register on_event"
90
90
  end
91
91
 
92
- completion = Phronomy::Task.deferred(
92
+ completion = Phronomy::TaskResult.deferred(
93
93
  name: "agent-recovery-load:#{execution.execution_id}"
94
94
  )
95
95
  command = InstallCommand.new(
@@ -140,7 +140,7 @@ module Phronomy
140
140
  failure = error_present ?
141
141
  RecoverySupport.resolution_failure(error) : nil
142
142
 
143
- completion = Phronomy::Task.deferred(
143
+ completion = Phronomy::TaskResult.deferred(
144
144
  name: "agent-recovery-resolve:#{execution_id}"
145
145
  )
146
146
  command = ResolveCommand.new(
@@ -163,7 +163,7 @@ module Phronomy
163
163
  end
164
164
  completion
165
165
  rescue => caught
166
- completion ||= Phronomy::Task.deferred(
166
+ completion ||= Phronomy::TaskResult.deferred(
167
167
  name: "agent-recovery-resolve:#{execution_id}"
168
168
  )
169
169
  completion.fail(caught)
@@ -4,15 +4,15 @@ module Phronomy
4
4
  module Agent
5
5
  # Routes Tool work according to the Tool execution contract.
6
6
  #
7
- # Both execution modes return {Phronomy::Task}; only the execution mechanism
7
+ # Both execution modes return {Phronomy::TaskResult}; only the execution mechanism
8
8
  # differs.
9
9
  #
10
10
  # :cooperative Tool calls execute inline and must return quickly. call_async
11
- # wraps their result in an already-settled Task and never consumes an
11
+ # wraps their result in an already-settled TaskResult and never consumes an
12
12
  # OffloadPool worker.
13
13
  #
14
14
  # :offloaded Tool calls route synchronous work through OffloadPool, whose
15
- # caller-facing completion handle is also a Task. Phronomy does not distinguish
15
+ # caller-facing completion handle is also a TaskResult. Phronomy does not distinguish
16
16
  # whether the reason is blocking I/O, CPU-bound work, or another long
17
17
  # synchronous operation.
18
18
  module ToolExecutor
@@ -28,7 +28,7 @@ module Phronomy
28
28
 
29
29
  case mode
30
30
  when :cooperative
31
- task = Phronomy::Task.deferred(name: "tool-#{tool.name}")
31
+ task = Phronomy::TaskResult.deferred(name: "tool-#{tool.name}")
32
32
  begin
33
33
  task.complete(
34
34
  tool.call(args, cancellation_token: cancellation_token)
@@ -502,7 +502,7 @@ module Phronomy
502
502
  value.is_a?(Phronomy::WorkflowRunner) ||
503
503
  value.is_a?(Phronomy::WorkflowContext) ||
504
504
  value.is_a?(Phronomy::Runtime) ||
505
- value.is_a?(Phronomy::Task) ||
505
+ value.is_a?(Phronomy::TaskResult) ||
506
506
  value.is_a?(Phronomy::EventLoop) ||
507
507
  value.is_a?(Phronomy::FSMSession) ||
508
508
  value.is_a?(Phronomy::FSMSession::EventSink) ||
@@ -181,9 +181,9 @@ module Phronomy
181
181
  def self.build_entry_callback(callable, state_name)
182
182
  ->(machine) {
183
183
  result = callable.call(machine.context)
184
- if result.is_a?(Phronomy::Task)
184
+ if result.is_a?(Phronomy::TaskResult)
185
185
  raise Phronomy::InvalidAsyncEntryActionError,
186
- "Tool entry action for #{state_name.inspect} returned Phronomy::Task"
186
+ "Tool entry action for #{state_name.inspect} returned Phronomy::TaskResult"
187
187
  end
188
188
  machine.context = result if result.respond_to?(:set_graph_metadata)
189
189
  }
@@ -7,27 +7,34 @@ module Phronomy
7
7
  module Blocking
8
8
  # Submits synchronous application work to the default Runtime OffloadPool.
9
9
  # Admission never waits for queue space. Admission StandardError failures
10
- # become failed Tasks; accepted work retains the pool's Task unchanged.
11
- # A timeout/cancellation can settle that Task before a running worker exits.
12
- # The block must not wait for another Agent, Workflow, or Task.
10
+ # become failed results; accepted work retains the pool's TaskResult unchanged.
11
+ # A timeout/cancellation can settle that TaskResult before a running worker exits.
12
+ # The block must not wait for another Agent, Workflow, or TaskResult.
13
13
  # @param timeout [Numeric, nil] operation deadline, including queue time
14
14
  # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
15
+ # @param invocation_context [Phronomy::InvocationContext, nil] explicit context/scope
15
16
  # @yield synchronous application work
16
- # @return [Phronomy::Task] original accepted Task or failed admission Task
17
+ # @return [Phronomy::TaskResult] original accepted TaskResult or failed admission TaskResult
17
18
  # @raise [ArgumentError] if no block is supplied
18
19
  # @api public
19
- def self.call_async(timeout: nil, cancellation_token: nil, &block)
20
+ def self.call_async(timeout: nil, cancellation_token: nil, invocation_context: nil, &block)
20
21
  raise ArgumentError, "Blocking.call_async requires a block" unless block
21
22
 
22
23
  begin
23
- Phronomy::Runtime.instance.offload.submit(
24
+ unless invocation_context.nil?
25
+ binding = Execution.__operation_binding(invocation_context: invocation_context,
26
+ cancellation_token: cancellation_token)
27
+ end
28
+ result = Phronomy::Runtime.instance.offload.submit(
24
29
  on_full: :raise,
25
30
  timeout: timeout,
26
- cancellation_token: cancellation_token,
31
+ cancellation_token: binding ? binding.token : cancellation_token,
27
32
  &block
28
33
  )
34
+ binding ? binding.bind(result) : result
29
35
  rescue => error
30
- Phronomy::Task.failed(error, name: "blocking-admission-failed")
36
+ result = Phronomy::TaskResult.failed(error, name: "blocking-admission-failed")
37
+ binding ? binding.bind(result) : result
31
38
  end
32
39
  end
33
40
  end