phronomy 0.31.0 → 0.32.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 +64 -0
- data/README.md +7 -1
- data/docs/features.md +2 -0
- data/docs/runtime-and-concurrency.md +159 -0
- data/lib/phronomy/agent/approval_wait.rb +8 -0
- data/lib/phronomy/agent/async_event_api.rb +60 -0
- data/lib/phronomy/agent/base.rb +1 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +4 -0
- data/lib/phronomy/agent/execution/execution_coordinator.rb +89 -2
- data/lib/phronomy/agent/journal/journal_projection.rb +24 -1
- data/lib/phronomy/agent/lifecycle/hosted_invocation.rb +70 -0
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +30 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +7 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +12 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +86 -14
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +1 -1
- data/lib/phronomy/agent/store.rb +52 -0
- data/lib/phronomy/context/context_policy_input_builder.rb +27 -19
- data/lib/phronomy/engine/fsm_session.rb +9 -3
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +87 -5
- data/lib/phronomy/multi_agent/orchestrator.rb +30 -18
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +9 -0
- data/lib/phronomy/workflow/execution/workflow_runner.rb +62 -14
- data/scripts/api_snapshot.rb +2 -0
- data/sig/phronomy/agent/execution_contract.rbs +13 -0
- data/sig/phronomy/agent/store.rbs +1 -0
- data/sig/phronomy/agent.rbs +1 -0
- data/sig/phronomy/multi_agent.rbs +2 -0
- data/sig/phronomy/workflow.rbs +1 -0
- metadata +4 -2
data/lib/phronomy/version.rb
CHANGED
|
@@ -45,6 +45,15 @@ module Phronomy
|
|
|
45
45
|
@runner.send_event(state: state, event: event, input: input)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Applies an explicitly named event to one revision of a saved wait.
|
|
49
|
+
# The host holds its exclusive claim for this Workflow across the call.
|
|
50
|
+
# @return [WorkflowContext] the next halted or completed context
|
|
51
|
+
# @api public
|
|
52
|
+
def send_saved_event(workflow_instance_id:, expected_revision:, event:, input: nil)
|
|
53
|
+
@runner.send_saved_event(workflow_instance_id: workflow_instance_id,
|
|
54
|
+
expected_revision: expected_revision, event: event, input: input)
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
# Sends an event to an active Workflow execution without blocking.
|
|
49
58
|
#
|
|
50
59
|
# +workflow_instance_id+ is the logical/durable Workflow identity. EventLoop resolves it
|
|
@@ -54,6 +54,7 @@ module Phronomy
|
|
|
54
54
|
:input,
|
|
55
55
|
:event_name,
|
|
56
56
|
:current_phase,
|
|
57
|
+
:expected_revision,
|
|
57
58
|
:result_task,
|
|
58
59
|
:workflow_instance_id,
|
|
59
60
|
:owner_token
|
|
@@ -141,13 +142,28 @@ module Phronomy
|
|
|
141
142
|
end
|
|
142
143
|
|
|
143
144
|
start_resume_execution(
|
|
144
|
-
|
|
145
|
+
workflow_instance_id,
|
|
146
|
+
state: state,
|
|
145
147
|
input: input,
|
|
146
148
|
event_name: event_name,
|
|
147
149
|
current_phase: current_phase
|
|
148
150
|
).wait_result
|
|
149
151
|
end
|
|
150
152
|
|
|
153
|
+
def send_saved_event(workflow_instance_id:, expected_revision:, event:, input: nil)
|
|
154
|
+
if Phronomy::WaitPolicy.blocking_forbidden?
|
|
155
|
+
raise Phronomy::EventLoopReentrancyError, "send_saved_event requires a host thread"
|
|
156
|
+
end
|
|
157
|
+
unless workflow_instance_id.is_a?(String) && !workflow_instance_id.empty?
|
|
158
|
+
raise ArgumentError, "workflow_instance_id must be a nonempty String"
|
|
159
|
+
end
|
|
160
|
+
unless expected_revision.is_a?(Integer) && expected_revision.positive?
|
|
161
|
+
raise ArgumentError, "expected_revision must be a positive Integer"
|
|
162
|
+
end
|
|
163
|
+
start_resume_execution(workflow_instance_id.dup, expected_revision: expected_revision,
|
|
164
|
+
input: input, event_name: event).wait_result
|
|
165
|
+
end
|
|
166
|
+
|
|
151
167
|
# Posts an application-defined event to the currently live Workflow owner.
|
|
152
168
|
# WorkflowExecutionRegistry resolves workflow_instance_id to the current concrete
|
|
153
169
|
# fsm_session_id. The opaque admission owner token is deliberately not a
|
|
@@ -256,8 +272,8 @@ module Phronomy
|
|
|
256
272
|
result_task || failed_task("workflow:preparation", error)
|
|
257
273
|
end
|
|
258
274
|
|
|
259
|
-
def start_resume_execution(
|
|
260
|
-
workflow_instance_id =
|
|
275
|
+
def start_resume_execution(workflow_instance_id, input:, event_name:, state: nil, current_phase: nil, expected_revision: nil)
|
|
276
|
+
workflow_instance_id = workflow_instance_id.to_s.freeze
|
|
261
277
|
result_task = Phronomy::TaskResult.deferred(name: "workflow-resume:#{workflow_instance_id}")
|
|
262
278
|
Phronomy::Tracing::Automatic.observe_task(
|
|
263
279
|
result_task,
|
|
@@ -272,7 +288,8 @@ module Phronomy
|
|
|
272
288
|
state: state,
|
|
273
289
|
input: input,
|
|
274
290
|
event_name: event_name.to_sym,
|
|
275
|
-
current_phase: current_phase
|
|
291
|
+
current_phase: current_phase,
|
|
292
|
+
expected_revision: expected_revision,
|
|
276
293
|
result_task: result_task,
|
|
277
294
|
workflow_instance_id: workflow_instance_id,
|
|
278
295
|
owner_token: Object.new.freeze
|
|
@@ -345,6 +362,7 @@ module Phronomy
|
|
|
345
362
|
|
|
346
363
|
repository = configured_repository
|
|
347
364
|
unless repository
|
|
365
|
+
raise Phronomy::ConfigurationError, "send_saved_event requires Workflow persistence" if request.expected_revision
|
|
348
366
|
context = request.input ? request.state.merge(request.input) : request.state
|
|
349
367
|
execution = Execution.new(
|
|
350
368
|
context: context,
|
|
@@ -445,12 +463,9 @@ module Phronomy
|
|
|
445
463
|
return
|
|
446
464
|
end
|
|
447
465
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
ready.result.revision
|
|
452
|
-
)
|
|
453
|
-
context = request.input ? request.state.merge(request.input) : request.state
|
|
466
|
+
state = request.state || restore_saved_wait!(request, ready.result)
|
|
467
|
+
expected_revision = validate_resume_snapshot!(state, ready.result.snapshot, ready.result.revision)
|
|
468
|
+
context = request.input ? state.merge(request.input) : state
|
|
454
469
|
execution = Execution.new(
|
|
455
470
|
context: context,
|
|
456
471
|
workflow_instance_id: request.workflow_instance_id,
|
|
@@ -465,12 +480,28 @@ module Phronomy
|
|
|
465
480
|
execution,
|
|
466
481
|
request.result_task,
|
|
467
482
|
resume_event: request.event_name,
|
|
468
|
-
resume_phase: request.current_phase
|
|
483
|
+
resume_phase: request.current_phase || state.phase,
|
|
484
|
+
require_resume_transition: !request.expected_revision.nil?
|
|
469
485
|
)
|
|
470
486
|
rescue => error
|
|
471
487
|
release_and_fail(request, error)
|
|
472
488
|
end
|
|
473
489
|
|
|
490
|
+
def restore_saved_wait!(request, loaded)
|
|
491
|
+
unless loaded.snapshot && loaded.revision == request.expected_revision
|
|
492
|
+
raise Phronomy::Persistence::StateConflictError,
|
|
493
|
+
"No saved Workflow at revision #{request.expected_revision} for #{request.workflow_instance_id.inspect}"
|
|
494
|
+
end
|
|
495
|
+
phase = record_value(loaded.snapshot, :phase)&.to_sym
|
|
496
|
+
unless @wait_state_names.include?(phase) && @external_events.fetch(request.event_name, []).any? { |edge| edge[:from] == phase }
|
|
497
|
+
raise ArgumentError, "No saved wait at #{phase.inspect} accepting #{request.event_name.inspect}"
|
|
498
|
+
end
|
|
499
|
+
fields = record_value(loaded.snapshot, :fields)
|
|
500
|
+
@state_class.new(**restore_context_fields(fields)).set_graph_metadata(
|
|
501
|
+
workflow_instance_id: request.workflow_instance_id, phase: phase
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
|
|
474
505
|
def build_new_execution(
|
|
475
506
|
input,
|
|
476
507
|
workflow_instance_id:,
|
|
@@ -487,9 +518,7 @@ module Phronomy
|
|
|
487
518
|
stored_fields = loaded_snapshot &&
|
|
488
519
|
(loaded_snapshot[:fields] || loaded_snapshot["fields"])
|
|
489
520
|
initial_fields = if stored_fields
|
|
490
|
-
stored_fields
|
|
491
|
-
.transform_keys(&:to_sym)
|
|
492
|
-
.merge(input.transform_keys(&:to_sym))
|
|
521
|
+
restore_context_fields(stored_fields).merge(input.transform_keys(&:to_sym))
|
|
493
522
|
else
|
|
494
523
|
input
|
|
495
524
|
end
|
|
@@ -509,6 +538,21 @@ module Phronomy
|
|
|
509
538
|
)
|
|
510
539
|
end
|
|
511
540
|
|
|
541
|
+
# The repository and the load message own immutable snapshots. Actions receive
|
|
542
|
+
# their own mutable field values, including strings nested in arrays or hashes.
|
|
543
|
+
def restore_context_fields(fields)
|
|
544
|
+
fields.to_h { |name, value| [name.to_sym, mutable_field_value(value)] }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def mutable_field_value(value)
|
|
548
|
+
case value
|
|
549
|
+
when Hash then value.transform_values { |child| mutable_field_value(child) }
|
|
550
|
+
when Array then value.map { |child| mutable_field_value(child) }
|
|
551
|
+
when String then value.dup
|
|
552
|
+
else value
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
512
556
|
def validate_resume_snapshot!(state, durable_snapshot, durable_revision)
|
|
513
557
|
return nil unless durable_snapshot
|
|
514
558
|
|
|
@@ -576,6 +620,7 @@ module Phronomy
|
|
|
576
620
|
result_task,
|
|
577
621
|
resume_event: nil,
|
|
578
622
|
resume_phase: nil,
|
|
623
|
+
require_resume_transition: false,
|
|
579
624
|
stable_observer: nil
|
|
580
625
|
)
|
|
581
626
|
assert_execution_owner!
|
|
@@ -588,6 +633,7 @@ module Phronomy
|
|
|
588
633
|
execution: execution,
|
|
589
634
|
resume_event: resume_event,
|
|
590
635
|
resume_phase: resume_phase,
|
|
636
|
+
require_resume_transition: require_resume_transition,
|
|
591
637
|
stable_observer: stable_observer,
|
|
592
638
|
task_execution: task_execution
|
|
593
639
|
)
|
|
@@ -769,6 +815,7 @@ module Phronomy
|
|
|
769
815
|
execution:,
|
|
770
816
|
resume_event: nil,
|
|
771
817
|
resume_phase: nil,
|
|
818
|
+
require_resume_transition: false,
|
|
772
819
|
stable_observer: nil,
|
|
773
820
|
task_execution: nil
|
|
774
821
|
)
|
|
@@ -787,6 +834,7 @@ module Phronomy
|
|
|
787
834
|
recursion_limit: execution.recursion_limit,
|
|
788
835
|
resume_event: resume_event,
|
|
789
836
|
resume_phase: resume_phase,
|
|
837
|
+
require_resume_transition: require_resume_transition,
|
|
790
838
|
stable_observer: stable_observer,
|
|
791
839
|
persist: terminal_persistence_callback(execution),
|
|
792
840
|
task_execution: task_execution
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -30,6 +30,8 @@ PUBLIC_API_ENTRIES = [
|
|
|
30
30
|
Phronomy::Embeddings::AuthenticationError,
|
|
31
31
|
Phronomy::Embeddings::RateLimitError,
|
|
32
32
|
Phronomy::Agent::Base,
|
|
33
|
+
Phronomy::Agent::HostedInvocation,
|
|
34
|
+
Phronomy::Agent::ApprovalWait,
|
|
33
35
|
Phronomy::Agent::ReservedExecution,
|
|
34
36
|
Phronomy::Agent::ExecutionObservation,
|
|
35
37
|
Phronomy::Agent::KnowledgeItem,
|
|
@@ -4,6 +4,19 @@ module Phronomy
|
|
|
4
4
|
type execution_wiring = { ?phronomy_execution_participant: _ExecutionParticipant, ?phronomy_control_bindings: Array[_ControlCapability], ?phronomy_transfer_context: TransferContext, ?phronomy_admission: _AdmissionOwner, ?phronomy_reservation: Hash[String, json_value] }
|
|
5
5
|
type execution_result = { agent_id: String, execution_id: String, status: Symbol, result: json_value, output: json_value, ?raw_output: String?, error: Hash[String, json_value]? }
|
|
6
6
|
|
|
7
|
+
class ApprovalWait < Data
|
|
8
|
+
attr_reader execution_id: String
|
|
9
|
+
attr_reader approval_request_id: String
|
|
10
|
+
attr_reader execution_revision: Integer
|
|
11
|
+
def self.new: (execution_id: String, approval_request_id: String, execution_revision: Integer) -> instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class HostedInvocation
|
|
15
|
+
def on_complete: () { (untyped, StandardError?) -> untyped } -> self
|
|
16
|
+
def detach!: (approval_request_id: String, ?execution_id: String) -> ApprovalWait
|
|
17
|
+
def wait_result: (?timeout: Numeric?) -> (execution_result | ApprovalWait)
|
|
18
|
+
end
|
|
19
|
+
|
|
7
20
|
class ReservedExecution < Data
|
|
8
21
|
attr_reader agent_id: String
|
|
9
22
|
attr_reader execution_id: String
|
|
@@ -54,6 +54,7 @@ module Phronomy
|
|
|
54
54
|
def authorized_operations: (::Phronomy::Persistence::Transaction scope, agent_id: String, execution_id: String, invocation_id: String, name: String, arguments: Hash[String, untyped], names: Array[String]) -> Array[AuthorizedOperation]
|
|
55
55
|
def result: (String execution_id) -> Hash[Symbol, untyped]
|
|
56
56
|
def runs: (String owner_id, ?after: String?, ?limit: Integer) -> Array[untyped]
|
|
57
|
+
def standalone_approval_wait: (agent_id: String, execution_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> ApprovalWait?
|
|
57
58
|
def observe_execution: (agent_id: String, execution_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> ExecutionObservation
|
|
58
59
|
def active_executions: (agent_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> Array[ExecutionObservation]
|
|
59
60
|
def definition: (agent_id: String, ?scope: ::Phronomy::Persistence::Transaction?) -> { id: String, version: Integer }
|
data/sig/phronomy/agent.rbs
CHANGED
|
@@ -108,6 +108,7 @@ module Phronomy
|
|
|
108
108
|
attr_reader persistence: Store
|
|
109
109
|
def knowledge_snapshot: () -> Array[KnowledgeItem]
|
|
110
110
|
def event_listener: () -> (^(Object) -> void)?
|
|
111
|
+
def start_hosted: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> HostedInvocation
|
|
111
112
|
def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?, ?schema_delivery: (:native | :none)?) -> TaskResult[execution_result]
|
|
112
113
|
def resume_async: (String execution_id, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
|
|
113
114
|
def resume: (String execution_id, ?config: Hash[Symbol, Object]) -> execution_result
|
|
@@ -23,6 +23,8 @@ module Phronomy
|
|
|
23
23
|
def self.create: (?coordination_store: Store?, **Object options) ?{ (Object) -> void } -> instance
|
|
24
24
|
def self.load: (String agent_id, persistence: Agent::Store, coordination_store: Store, **Object options) ?{ (Object) -> void } -> instance
|
|
25
25
|
def self.subagent: (Symbol | String name, singleton(::Phronomy::Agent::Base) agent_class, ?on_error: Symbol, ?inherit_knowledge: bool) -> untyped
|
|
26
|
+
def unload_with_completed_child!: (String parent_execution_id, execution_id: String) -> bool
|
|
27
|
+
def approve_hosted_child: (String parent_execution_id, execution_id: String, approval_request_id: String, ?approved: bool, ?config: Hash[Symbol, untyped]) -> ::Phronomy::Agent::HostedInvocation
|
|
26
28
|
def self.registered_subagents: () -> Hash[Symbol | String, untyped]
|
|
27
29
|
def resume: (String execution_id, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
28
30
|
def dispatch_parallel: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
|
data/sig/phronomy/workflow.rbs
CHANGED
|
@@ -18,6 +18,7 @@ module Phronomy
|
|
|
18
18
|
def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) { (untyped) -> void } -> untyped
|
|
19
19
|
def resume: (state: untyped, ?input: untyped) -> untyped
|
|
20
20
|
def send_event: (state: untyped, event: Symbol, ?input: untyped) -> untyped
|
|
21
|
+
def send_saved_event: (workflow_instance_id: String, expected_revision: Integer, event: Symbol, ?input: untyped) -> untyped
|
|
21
22
|
def signal: (workflow_instance_id: String, event: Symbol, ?payload: untyped) -> bool
|
|
22
23
|
|
|
23
24
|
class Builder
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phronomy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.32.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raizo T.C.S
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -389,6 +389,7 @@ files:
|
|
|
389
389
|
- lib/phronomy/agent/agent_unloaded_error.rb
|
|
390
390
|
- lib/phronomy/agent/api/agent.rb
|
|
391
391
|
- lib/phronomy/agent/api/execution_rehydration_required_error.rb
|
|
392
|
+
- lib/phronomy/agent/approval_wait.rb
|
|
392
393
|
- lib/phronomy/agent/async_event_api.rb
|
|
393
394
|
- lib/phronomy/agent/base.rb
|
|
394
395
|
- lib/phronomy/agent/before_llm_input.rb
|
|
@@ -429,6 +430,7 @@ files:
|
|
|
429
430
|
- lib/phronomy/agent/knowledge_item.rb
|
|
430
431
|
- lib/phronomy/agent/lifecycle/agent_root.rb
|
|
431
432
|
- lib/phronomy/agent/lifecycle/default_persistence.rb
|
|
433
|
+
- lib/phronomy/agent/lifecycle/hosted_invocation.rb
|
|
432
434
|
- lib/phronomy/agent/lifecycle/lifetime_api.rb
|
|
433
435
|
- lib/phronomy/agent/lifecycle/operation_lease.rb
|
|
434
436
|
- lib/phronomy/agent/lifecycle/operation_result.rb
|