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
|
@@ -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
|
|
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
|
-
- `
|
|
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::
|
|
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
|
-
├─>
|
|
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
|
|
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
|
|
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::
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
##
|
|
348
|
+
## TaskResult
|
|
349
349
|
|
|
350
|
-
`Phronomy::
|
|
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
|
-
|
|
356
|
+
TaskResult is the common completion abstraction for logical EventLoop/FSMSession
|
|
357
357
|
lifecycles and OffloadPool-backed synchronous work.
|
|
358
358
|
|
|
359
|
-
`
|
|
360
|
-
timeout is waiter-local: it does not settle/cancel the
|
|
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
|
-
`
|
|
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
|
-
`
|
|
371
|
-
the mapped
|
|
370
|
+
`TaskResult#map` is application-level composition. A transformation exception settles
|
|
371
|
+
the mapped TaskResult as failed.
|
|
372
372
|
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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::
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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::
|
|
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
|
|
477
|
-
|
|
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::
|
|
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
|
|
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -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
|
-
|
|
802
|
+
invocation_context ?
|
|
803
803
|
config.merge(invocation_context: invocation_context) : config
|
|
804
804
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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.
|
|
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::
|
|
143
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
144
144
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
145
|
-
"Agent entry action for #{state_name.inspect} returned Phronomy::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
|
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
|
|
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::
|
|
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::
|
|
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::
|
|
184
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
185
185
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
186
|
-
"Tool entry action for #{state_name.inspect} returned Phronomy::
|
|
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
|
}
|
data/lib/phronomy/blocking.rb
CHANGED
|
@@ -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
|
|
11
|
-
# A timeout/cancellation can settle that
|
|
12
|
-
# The block must not wait for another Agent, Workflow, or
|
|
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::
|
|
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
|
-
|
|
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::
|
|
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
|