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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +215 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +147 -0
  15. data/docs/architecture/persistence.md +186 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +89 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +114 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/028-preparing-recovery-replay-contract.md +106 -0
  45. data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
  46. data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
  47. data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
  48. data/docs/decisions/README.md +176 -0
  49. data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
  50. data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
  51. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
  52. data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
  53. data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
  54. data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
  55. data/docs/features.md +65 -11
  56. data/docs/getting-started.md +77 -45
  57. data/docs/migrations/0.19.md +14 -7
  58. data/docs/migrations/0.22.md +390 -0
  59. data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
  60. data/docs/persistence-backends.md +129 -40
  61. data/docs/runtime-and-concurrency.md +227 -33
  62. data/examples/README.md +13 -0
  63. data/lib/phronomy/agent/agent_execution.rb +21 -17
  64. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  65. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  66. data/lib/phronomy/agent/agent_root.rb +3 -3
  67. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  68. data/lib/phronomy/agent/async_event_api.rb +162 -72
  69. data/lib/phronomy/agent/base.rb +416 -181
  70. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  71. data/lib/phronomy/agent/context_assembler.rb +437 -178
  72. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  73. data/lib/phronomy/agent/context_plan.rb +18 -13
  74. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  75. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  76. data/lib/phronomy/agent/context_policy.rb +109 -3
  77. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  78. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  79. data/lib/phronomy/agent/exact_execution.rb +153 -0
  80. data/lib/phronomy/agent/execution_cancellation.rb +25 -0
  81. data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
  82. data/lib/phronomy/agent/handoff.rb +43 -0
  83. data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
  84. data/lib/phronomy/agent/handoff_context.rb +120 -0
  85. data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
  86. data/lib/phronomy/agent/handoff_policy.rb +143 -0
  87. data/lib/phronomy/agent/handoff_projection.rb +207 -0
  88. data/lib/phronomy/agent/handoff_request.rb +45 -0
  89. data/lib/phronomy/agent/handoff_runner.rb +178 -0
  90. data/lib/phronomy/agent/handoff_state.rb +43 -0
  91. data/lib/phronomy/agent/journal_record.rb +17 -4
  92. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  93. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  94. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  95. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  96. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  97. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +174 -0
  98. data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
  99. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
  100. data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
  101. data/lib/phronomy/agent/recovery_support.rb +504 -0
  102. data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -13
  103. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  104. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  105. data/lib/phronomy/agent/shared_state.rb +38 -1
  106. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  107. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  108. data/lib/phronomy/agent/tool_invocation.rb +339 -103
  109. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  110. data/lib/phronomy/agent.rb +20 -2
  111. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  112. data/lib/phronomy/agent_purged_error.rb +5 -0
  113. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  114. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  115. data/lib/phronomy/engine/event_loop.rb +622 -63
  116. data/lib/phronomy/engine/fsm_session.rb +194 -21
  117. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  118. data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
  119. data/lib/phronomy/engine/runtime.rb +92 -20
  120. data/lib/phronomy/generator_verifier.rb +12 -14
  121. data/lib/phronomy/invocation_context.rb +9 -29
  122. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  123. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
  124. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  125. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  126. data/lib/phronomy/multi_agent/orchestrator.rb +71 -26
  127. data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
  128. data/lib/phronomy/multi_agent/team_execution.rb +44 -0
  129. data/lib/phronomy/multi_agent/team_root.rb +41 -0
  130. data/lib/phronomy/persistence/durable_codec.rb +706 -0
  131. data/lib/phronomy/persistence/durable_record.rb +117 -0
  132. data/lib/phronomy/persistence/in_memory.rb +471 -133
  133. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  134. data/lib/phronomy/persistence/repository_facades.rb +535 -0
  135. data/lib/phronomy/persistence.rb +174 -40
  136. data/lib/phronomy/recovery.rb +186 -0
  137. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  138. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +2 -1
  139. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  140. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  141. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  142. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +137 -0
  143. data/lib/phronomy/testing/persistence_contract.rb +5 -0
  144. data/lib/phronomy/tools/agent.rb +1 -1
  145. data/lib/phronomy/tracing/automatic.rb +176 -0
  146. data/lib/phronomy/tracing/base.rb +11 -2
  147. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  148. data/lib/phronomy/version.rb +1 -1
  149. data/lib/phronomy/workflow.rb +3 -6
  150. data/lib/phronomy/workflow_context.rb +14 -5
  151. data/lib/phronomy/workflow_recovery.rb +123 -0
  152. data/lib/phronomy/workflow_runner.rb +468 -256
  153. data/lib/phronomy.rb +6 -0
  154. data/scripts/api_snapshot.rb +12 -0
  155. data/sig/phronomy/agent.rbs +209 -7
  156. data/sig/phronomy/handoff.rbs +41 -0
  157. data/sig/phronomy/multi_agent.rbs +35 -0
  158. data/sig/phronomy/persistence.rbs +123 -4
  159. data/sig/phronomy/runtime.rbs +1 -4
  160. data/sig/phronomy/workflow.rbs +2 -2
  161. data/sig/phronomy.rbs +10 -0
  162. metadata +84 -18
  163. data/examples/workflows/agent_event_mapping.rb +0 -101
  164. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  165. data/lib/phronomy/agent/activation_registry.rb +0 -28
  166. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  167. data/lib/phronomy/agent/context_candidate.rb +0 -47
  168. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  169. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  170. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  171. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  172. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  173. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  174. data/lib/phronomy/agent/context_request.rb +0 -35
  175. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  176. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  177. data/lib/phronomy/agent/runner.rb +0 -97
  178. 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, :thread_id)
7
+ Child = Data.define(:index, :agent, :input, :config)
10
8
 
11
- attr_reader :id, :phase, :results
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) # indexed by input order
23
- @fatal_error = nil # driver_failed / timeout / cancel
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(thread_id: nil, phase: nil)
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] = {handle: handle, token: child_config[:cancellation_token]}
83
- # [child.index].each creates a block parameter with a unique binding
84
- # per iteration, avoiding the while-loop variable capture problem.
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
- runtime.event_loop.post_to_session(
88
- Phronomy::Event.new(
89
- type: :child_completed,
90
- target_id: @id,
91
- payload: {index: captured_index, result: result, error: error}
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
- runtime.event_loop.post_to_session(
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
- @on_error == :raise && @pending.empty? && @active.empty? && @child_errors.any?(&:itself)
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:#{invocation.id}")
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
- source = Phronomy::Task.deferred(name: "fan-out-source:#{invocation.id}")
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
- runtime.event_loop.post_to_session(
43
- Phronomy::Event.new(
44
- type: :timeout,
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
- runtime.event_loop.post_to_session(
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
- phase_machine = build_phase_machine(runtime)
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, thread_id: thread_id}
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, thread_id: thread_id}
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
- captured_context[:knowledge] = active_knowledge_snapshot if inherits_knowledge
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.merge!(
237
- thread_id: invocation.thread_id,
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