phronomy 0.22.0 → 0.24.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/.mutant.yml +3 -4
- data/CHANGELOG.md +215 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +147 -0
- data/docs/architecture/persistence.md +186 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +89 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
- data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
- data/docs/decisions/README.md +176 -0
- data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
- data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
- data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
- data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
- data/docs/features.md +65 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +129 -40
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +21 -17
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +162 -72
- data/lib/phronomy/agent/base.rb +416 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/exact_execution.rb +153 -0
- data/lib/phronomy/agent/execution_cancellation.rb +25 -0
- data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
- data/lib/phronomy/agent/handoff.rb +43 -0
- data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
- data/lib/phronomy/agent/handoff_context.rb +120 -0
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
- data/lib/phronomy/agent/handoff_policy.rb +143 -0
- data/lib/phronomy/agent/handoff_projection.rb +207 -0
- data/lib/phronomy/agent/handoff_request.rb +45 -0
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
- data/lib/phronomy/agent/recovery_support.rb +504 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +339 -103
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +92 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
- data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
- data/lib/phronomy/multi_agent/team_execution.rb +44 -0
- data/lib/phronomy/multi_agent/team_root.rb +41 -0
- data/lib/phronomy/persistence/durable_codec.rb +706 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +471 -133
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +535 -0
- data/lib/phronomy/persistence.rb +174 -40
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
- data/lib/phronomy/testing/persistence_contract.rb +5 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +35 -0
- data/sig/phronomy/persistence.rbs +123 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +84 -18
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
- data/lib/phronomy/multi_agent/handoff.rb +0 -64
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
3
|
module Phronomy
|
|
6
4
|
module MultiAgent
|
|
7
5
|
# Mutable FSM context for one fan-out operation.
|
|
8
6
|
class FanOutInvocation
|
|
9
|
-
Child = Data.define(:index, :agent, :input, :config
|
|
7
|
+
Child = Data.define(:index, :agent, :input, :config)
|
|
10
8
|
|
|
11
|
-
attr_reader :
|
|
9
|
+
attr_reader :phase, :results
|
|
12
10
|
|
|
13
11
|
def initialize(children:, max_concurrency:, on_error:)
|
|
14
|
-
@id = SecureRandom.uuid.to_s
|
|
15
12
|
@phase = nil
|
|
16
13
|
@children = children
|
|
17
14
|
@max_concurrency = max_concurrency || children.length
|
|
@@ -19,15 +16,13 @@ module Phronomy
|
|
|
19
16
|
@pending = children.dup
|
|
20
17
|
@active = {}
|
|
21
18
|
@results = Array.new(children.length)
|
|
22
|
-
@child_errors = Array.new(children.length)
|
|
23
|
-
@fatal_error = nil
|
|
19
|
+
@child_errors = Array.new(children.length)
|
|
20
|
+
@fatal_error = nil
|
|
24
21
|
@cancelled = false
|
|
25
22
|
@timed_out = false
|
|
26
|
-
@session_id = nil
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
def set_graph_metadata(
|
|
30
|
-
@session_id = thread_id if thread_id
|
|
25
|
+
def set_graph_metadata(phase: nil)
|
|
31
26
|
@phase = phase
|
|
32
27
|
end
|
|
33
28
|
|
|
@@ -41,9 +36,6 @@ module Phronomy
|
|
|
41
36
|
if child_error
|
|
42
37
|
if @on_error == :raise
|
|
43
38
|
@child_errors[index] = child_error
|
|
44
|
-
# Signal cancellation so other children can stop early, but continue
|
|
45
|
-
# waiting for all active children to respond so we can return the
|
|
46
|
-
# first error in INPUT ORDER (not arrival order).
|
|
47
39
|
cancel_active_children! unless @child_errors.any?(&:itself)
|
|
48
40
|
end
|
|
49
41
|
else
|
|
@@ -69,27 +61,28 @@ module Phronomy
|
|
|
69
61
|
end
|
|
70
62
|
end
|
|
71
63
|
|
|
72
|
-
def start_available!(runtime)
|
|
64
|
+
def start_available!(runtime, event_sink:)
|
|
73
65
|
return self if @fatal_error
|
|
74
66
|
|
|
75
67
|
while @active.length < @max_concurrency && (child = @pending.shift)
|
|
76
68
|
child_config = build_child_config(child.config)
|
|
77
69
|
handle = child.agent.invoke_async(
|
|
78
70
|
child.input,
|
|
79
|
-
config: child_config
|
|
80
|
-
thread_id: child.thread_id
|
|
71
|
+
config: child_config
|
|
81
72
|
)
|
|
82
|
-
@active[child.index] = {
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
@active[child.index] = {
|
|
74
|
+
handle: handle,
|
|
75
|
+
token: child_config[:cancellation_token]
|
|
76
|
+
}
|
|
85
77
|
[child.index].each do |captured_index|
|
|
86
78
|
handle.on_complete do |result, error|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
event_sink.post(
|
|
80
|
+
:child_completed,
|
|
81
|
+
{
|
|
82
|
+
index: captured_index,
|
|
83
|
+
result: result,
|
|
84
|
+
error: error
|
|
85
|
+
}
|
|
93
86
|
)
|
|
94
87
|
end
|
|
95
88
|
end
|
|
@@ -98,13 +91,7 @@ module Phronomy
|
|
|
98
91
|
rescue => caught
|
|
99
92
|
@fatal_error ||= caught
|
|
100
93
|
cancel_active_children!
|
|
101
|
-
|
|
102
|
-
Phronomy::Event.new(
|
|
103
|
-
type: :driver_failed,
|
|
104
|
-
target_id: @id,
|
|
105
|
-
payload: {error: caught}
|
|
106
|
-
)
|
|
107
|
-
)
|
|
94
|
+
event_sink.post(:driver_failed, {error: caught})
|
|
108
95
|
self
|
|
109
96
|
end
|
|
110
97
|
|
|
@@ -114,7 +101,11 @@ module Phronomy
|
|
|
114
101
|
|
|
115
102
|
def failed?
|
|
116
103
|
return true if @fatal_error
|
|
117
|
-
|
|
104
|
+
|
|
105
|
+
@on_error == :raise &&
|
|
106
|
+
@pending.empty? &&
|
|
107
|
+
@active.empty? &&
|
|
108
|
+
@child_errors.any?(&:itself)
|
|
118
109
|
end
|
|
119
110
|
|
|
120
111
|
def error
|
|
@@ -16,14 +16,15 @@ module Phronomy
|
|
|
16
16
|
cancellation_token: nil,
|
|
17
17
|
runtime: Phronomy::Runtime.instance
|
|
18
18
|
)
|
|
19
|
-
result = Phronomy::Task.deferred(name: "fan-out
|
|
19
|
+
result = Phronomy::Task.deferred(name: "fan-out")
|
|
20
20
|
|
|
21
21
|
if cancellation_token&.cancelled?
|
|
22
22
|
result.fail(Phronomy::CancellationError.new("fan-out cancelled"))
|
|
23
23
|
return result
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
session = build(invocation: invocation, runtime: runtime)
|
|
27
|
+
source = Phronomy::Task.deferred(name: "fan-out-source:#{session.id}")
|
|
27
28
|
source.on_complete do |context, error|
|
|
28
29
|
if error
|
|
29
30
|
result.fail(error)
|
|
@@ -34,27 +35,18 @@ module Phronomy
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
session = build(invocation: invocation, runtime: runtime)
|
|
38
38
|
runtime.event_loop.register(session, completion: source)
|
|
39
|
-
|
|
40
39
|
if timeout
|
|
41
40
|
runtime.timer_queue.schedule(seconds: timeout) do
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
target_id: invocation.id,
|
|
46
|
-
payload: {message: "dispatch_parallel timed out after #{timeout}s"}
|
|
47
|
-
)
|
|
41
|
+
session.event_sink.post(
|
|
42
|
+
:timeout,
|
|
43
|
+
{message: "dispatch_parallel timed out after #{timeout}s"}
|
|
48
44
|
)
|
|
49
45
|
end
|
|
50
46
|
end
|
|
51
|
-
|
|
52
47
|
cancellation_token&.on_cancel do
|
|
53
|
-
|
|
54
|
-
Phronomy::Event.new(type: :cancel, target_id: invocation.id, payload: nil)
|
|
55
|
-
)
|
|
48
|
+
session.event_sink.post(:cancel, nil)
|
|
56
49
|
end
|
|
57
|
-
|
|
58
50
|
result
|
|
59
51
|
rescue => error
|
|
60
52
|
result ||= Phronomy::Task.deferred(name: "fan-out:registration")
|
|
@@ -63,7 +55,8 @@ module Phronomy
|
|
|
63
55
|
end
|
|
64
56
|
|
|
65
57
|
def self.build(invocation:, runtime:)
|
|
66
|
-
|
|
58
|
+
event_sink = Phronomy::FSMSession::EventSink.new(event_loop: runtime.event_loop)
|
|
59
|
+
phase_machine = build_phase_machine(runtime, event_sink)
|
|
67
60
|
external_events = {
|
|
68
61
|
child_completed: [{from: :running, to: :running, guard: nil}],
|
|
69
62
|
driver_failed: [{from: :running, to: :failed, guard: nil}],
|
|
@@ -72,8 +65,8 @@ module Phronomy
|
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
Phronomy::FSMSession.new(
|
|
75
|
-
id: invocation.id,
|
|
76
68
|
context: invocation,
|
|
69
|
+
event_sink: event_sink,
|
|
77
70
|
entry_point: :idle,
|
|
78
71
|
entry_actions: {},
|
|
79
72
|
auto_state_set: AUTO_STATE_SET,
|
|
@@ -87,7 +80,7 @@ module Phronomy
|
|
|
87
80
|
end
|
|
88
81
|
private_class_method :build
|
|
89
82
|
|
|
90
|
-
def self.build_phase_machine(runtime)
|
|
83
|
+
def self.build_phase_machine(runtime, event_sink)
|
|
91
84
|
Class.new do
|
|
92
85
|
attr_accessor :context, :current_event
|
|
93
86
|
|
|
@@ -114,7 +107,7 @@ module Phronomy
|
|
|
114
107
|
event(:cancel) { transition running: :cancelled }
|
|
115
108
|
|
|
116
109
|
after_transition to: :running do |machine|
|
|
117
|
-
machine.context.start_available!(runtime)
|
|
110
|
+
machine.context.start_available!(runtime, event_sink: event_sink)
|
|
118
111
|
end
|
|
119
112
|
end
|
|
120
113
|
end
|
|
@@ -6,11 +6,33 @@ module Phronomy
|
|
|
6
6
|
class Orchestrator < Agent::Base
|
|
7
7
|
agent_definition id: "orchestrator", version: 1
|
|
8
8
|
|
|
9
|
+
# Own one live cancellation token shared with this invocation's static
|
|
10
|
+
# children, including when Recovery reconstructs the invocation.
|
|
11
|
+
# @api private
|
|
12
|
+
def __invocation_config(config)
|
|
13
|
+
config.merge(cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Resumes retained static child coordination with current class wiring.
|
|
17
|
+
# @api public
|
|
18
|
+
def resume(execution_id, config: {})
|
|
19
|
+
if Phronomy::Runtime.in_event_loop_context?
|
|
20
|
+
raise Phronomy::EventLoopReentrancyError, "Orchestrator#resume cannot block EventLoop"
|
|
21
|
+
end
|
|
22
|
+
execution = persistence.executions.load(execution_id)
|
|
23
|
+
raise Phronomy::Persistence::ConflictError, "Parent execution owner mismatch" unless execution.agent_id == agent_id
|
|
24
|
+
input = persistence.contents.fetch_text(execution.metadata.fetch("current_input_ref"))
|
|
25
|
+
result = Phronomy::Agent::ExactExecution.start(agent: self, execution_id: execution_id, input: input, config: config).wait_result
|
|
26
|
+
raise Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]) if result[:error]
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
9
30
|
def self.subagent(name, agent_class, on_error: :raise, inherit_knowledge: true)
|
|
10
31
|
# A subagent Tool is logically asynchronous: ToolInvocation starts the
|
|
11
32
|
# child Agent and resumes when its completion Task settles. It must not
|
|
12
33
|
# occupy an OffloadPool worker while waiting for the child.
|
|
13
34
|
tool_class = Class.new(Phronomy::Tools::Agent) do
|
|
35
|
+
def self.__framework_owned_operation? = true
|
|
14
36
|
tool_name "dispatch_to_#{name}"
|
|
15
37
|
description "Dispatch work to the #{name} subagent (#{agent_class.name})"
|
|
16
38
|
|
|
@@ -24,6 +46,20 @@ module Phronomy
|
|
|
24
46
|
).wait_result
|
|
25
47
|
end
|
|
26
48
|
|
|
49
|
+
# Enter exact child reconciliation even when the parent token is
|
|
50
|
+
# cancelled: an already admitted child must be settled, not forgotten.
|
|
51
|
+
define_method(:call_async) do |args, cancellation_token: nil, config: {}|
|
|
52
|
+
if @_orchestrator_context&.fetch(:parent, nil) && config[:phronomy_tool_invocation_id]
|
|
53
|
+
validated, schema_error = send(:validate_and_coerce, args)
|
|
54
|
+
raise Phronomy::ToolError, schema_error if schema_error
|
|
55
|
+
execute_async(**validated, cancellation_token: cancellation_token, config: config)
|
|
56
|
+
else
|
|
57
|
+
super(args, cancellation_token: cancellation_token, config: config)
|
|
58
|
+
end
|
|
59
|
+
rescue => error
|
|
60
|
+
Phronomy::Task.deferred(name: "subagent-dispatch-failed").tap { |task| task.fail(error) }
|
|
61
|
+
end
|
|
62
|
+
|
|
27
63
|
define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
|
|
28
64
|
ctx = @_orchestrator_context || {}
|
|
29
65
|
parent_ic = ctx[:invocation_context]
|
|
@@ -38,21 +74,19 @@ module Phronomy
|
|
|
38
74
|
task_config = task_config.merge(invocation_context: child_ic)
|
|
39
75
|
end
|
|
40
76
|
|
|
77
|
+
parent = ctx[:parent]
|
|
78
|
+
if parent && task_config[:phronomy_tool_invocation_id]
|
|
79
|
+
return DurableSubagentCoordinator.start(parent: parent,
|
|
80
|
+
tool_invocation_id: task_config.fetch(:phronomy_tool_invocation_id),
|
|
81
|
+
parent_execution_id: task_config.fetch(:execution_id), config: task_config)
|
|
82
|
+
end
|
|
41
83
|
agent = agent_class.new
|
|
42
84
|
if inherit_knowledge
|
|
43
85
|
Array(ctx[:knowledge]).each do |entry|
|
|
44
|
-
agent.add_knowledge(
|
|
45
|
-
entry.fetch(:content),
|
|
46
|
-
metadata: entry.fetch(:metadata, {})
|
|
47
|
-
)
|
|
86
|
+
agent.add_knowledge(entry.fetch(:content), metadata: entry.fetch(:metadata, {}))
|
|
48
87
|
end
|
|
49
88
|
end
|
|
50
|
-
|
|
51
|
-
source = agent.invoke_async(
|
|
52
|
-
input,
|
|
53
|
-
thread_id: ctx[:thread_id] || parent_ic&.thread_id,
|
|
54
|
-
config: task_config
|
|
55
|
-
)
|
|
89
|
+
source = agent.invoke_async(input, config: task_config)
|
|
56
90
|
result_task = Phronomy::Task.deferred(
|
|
57
91
|
name: "subagent-tool-#{name}"
|
|
58
92
|
)
|
|
@@ -151,7 +185,6 @@ module Phronomy
|
|
|
151
185
|
agent:,
|
|
152
186
|
inputs:,
|
|
153
187
|
config: {},
|
|
154
|
-
thread_id: nil,
|
|
155
188
|
max_concurrency: nil,
|
|
156
189
|
on_error: :raise,
|
|
157
190
|
timeout: nil,
|
|
@@ -161,7 +194,7 @@ module Phronomy
|
|
|
161
194
|
)
|
|
162
195
|
dispatch_parallel(
|
|
163
196
|
*inputs.map do |input|
|
|
164
|
-
{agent: agent, input: input, config: config
|
|
197
|
+
{agent: agent, input: input, config: config}
|
|
165
198
|
end,
|
|
166
199
|
max_concurrency: max_concurrency,
|
|
167
200
|
on_error: on_error,
|
|
@@ -176,7 +209,6 @@ module Phronomy
|
|
|
176
209
|
agent:,
|
|
177
210
|
inputs:,
|
|
178
211
|
config: {},
|
|
179
|
-
thread_id: nil,
|
|
180
212
|
max_concurrency: nil,
|
|
181
213
|
on_error: :raise,
|
|
182
214
|
timeout: nil,
|
|
@@ -186,7 +218,7 @@ module Phronomy
|
|
|
186
218
|
)
|
|
187
219
|
dispatch_parallel_async(
|
|
188
220
|
*inputs.map do |input|
|
|
189
|
-
{agent: agent, input: input, config: config
|
|
221
|
+
{agent: agent, input: input, config: config}
|
|
190
222
|
end,
|
|
191
223
|
max_concurrency: max_concurrency,
|
|
192
224
|
on_error: on_error,
|
|
@@ -201,7 +233,6 @@ module Phronomy
|
|
|
201
233
|
agent_class,
|
|
202
234
|
input,
|
|
203
235
|
config: nil,
|
|
204
|
-
thread_id: nil,
|
|
205
236
|
inherit_knowledge: true
|
|
206
237
|
)
|
|
207
238
|
if Phronomy::Runtime.in_event_loop_context?
|
|
@@ -213,11 +244,21 @@ module Phronomy
|
|
|
213
244
|
inherit_knowledge: inherit_knowledge
|
|
214
245
|
).invoke_async(
|
|
215
246
|
input,
|
|
216
|
-
config: config || {}
|
|
217
|
-
thread_id: thread_id
|
|
247
|
+
config: config || {}
|
|
218
248
|
).wait_result
|
|
219
249
|
end
|
|
220
250
|
|
|
251
|
+
# @api private
|
|
252
|
+
def __framework_tool_replayable?(name)
|
|
253
|
+
self.class.registered_subagents.keys.any? { |key| "dispatch_to_#{key}" == name.to_s }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Reserve child identity/input/knowledge in the parent's existing Agent transaction.
|
|
257
|
+
# @api private
|
|
258
|
+
def __prepare_coordination_record(execution, tx:)
|
|
259
|
+
DurableSubagentCoordinator.prepare(self, execution, tx: tx)
|
|
260
|
+
end
|
|
261
|
+
|
|
221
262
|
private
|
|
222
263
|
|
|
223
264
|
def prepare_tool_class(tool_class, invocation: nil)
|
|
@@ -230,14 +271,13 @@ module Phronomy
|
|
|
230
271
|
end&.last
|
|
231
272
|
inherits_knowledge = registration ? registration.fetch(:inherit_knowledge, true) : true
|
|
232
273
|
|
|
233
|
-
captured_context = {}
|
|
234
|
-
|
|
274
|
+
captured_context = {parent: self}
|
|
275
|
+
# Invocation-owned Tools inherit the durable child slot's knowledge,
|
|
276
|
+
# captured by the existing preparation transaction off EventLoop.
|
|
277
|
+
captured_context[:knowledge] = active_knowledge_snapshot if inherits_knowledge && !invocation
|
|
235
278
|
if invocation
|
|
236
|
-
captured_context.
|
|
237
|
-
|
|
238
|
-
config: invocation.config,
|
|
239
|
-
invocation_context: invocation.config[:invocation_context]
|
|
240
|
-
)
|
|
279
|
+
captured_context[:config] = invocation.config
|
|
280
|
+
captured_context[:invocation_context] = invocation.config[:invocation_context]
|
|
241
281
|
end
|
|
242
282
|
captured_context.freeze
|
|
243
283
|
|
|
@@ -291,6 +331,12 @@ module Phronomy
|
|
|
291
331
|
raise ArgumentError, "fan-out task must be a Hash" unless task.is_a?(Hash)
|
|
292
332
|
raise ArgumentError, "fan-out task requires :agent" unless task[:agent]
|
|
293
333
|
raise ArgumentError, "fan-out task requires :input" unless task.key?(:input)
|
|
334
|
+
if task.key?(:thread_id) || task.key?("thread_id")
|
|
335
|
+
raise ArgumentError,
|
|
336
|
+
"fan-out task generic thread_id was removed; " \
|
|
337
|
+
"use purpose-specific domain identifiers or application tracing metadata"
|
|
338
|
+
end
|
|
339
|
+
_reject_removed_generic_identity_keys!(task.fetch(:config, {}))
|
|
294
340
|
end
|
|
295
341
|
end
|
|
296
342
|
|
|
@@ -324,8 +370,7 @@ module Phronomy
|
|
|
324
370
|
index: index,
|
|
325
371
|
agent: child_agent,
|
|
326
372
|
input: task.fetch(:input),
|
|
327
|
-
config: task_config
|
|
328
|
-
thread_id: task[:thread_id] || invocation_context&.thread_id
|
|
373
|
+
config: task_config
|
|
329
374
|
)
|
|
330
375
|
end
|
|
331
376
|
end
|