phronomy 0.23.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 +2 -2
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +2 -2
- data/README.md +1 -1
- data/docs/architecture/multi-agent-handoff.md +35 -40
- data/docs/architecture/persistence.md +19 -8
- data/docs/architecture.md +8 -1
- data/docs/decisions/016-semantic-multi-agent-handoff.md +3 -1
- 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 +5 -1
- 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 +35 -1
- data/docs/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +42 -3
- data/lib/phronomy/agent/agent_execution.rb +2 -2
- data/lib/phronomy/agent/agent_invocation.rb +1 -1
- data/lib/phronomy/agent/async_event_api.rb +18 -1
- data/lib/phronomy/agent/base.rb +28 -0
- data/lib/phronomy/agent/context_assembler.rb +1 -1
- 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 +484 -27
- data/lib/phronomy/{multi_agent → agent}/handoff.rb +4 -4
- data/lib/phronomy/{multi_agent → agent}/handoff_capability_factory.rb +3 -45
- data/lib/phronomy/{multi_agent → agent}/handoff_context.rb +26 -1
- data/lib/phronomy/{multi_agent/execution_coordinator.rb → agent/handoff_execution_coordinator.rb} +32 -5
- data/lib/phronomy/{multi_agent → agent}/handoff_policy.rb +7 -1
- data/lib/phronomy/{multi_agent → agent}/handoff_projection.rb +18 -2
- data/lib/phronomy/{multi_agent → agent}/handoff_request.rb +2 -2
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +114 -211
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +84 -130
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +76 -200
- data/lib/phronomy/agent/recovery_coordinator.rb +11 -5
- data/lib/phronomy/agent/recovery_support.rb +15 -23
- data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -8
- data/lib/phronomy/agent/tool_invocation.rb +4 -2
- data/lib/phronomy/engine/runtime/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +16 -1
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +134 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +59 -11
- 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 +60 -0
- data/lib/phronomy/persistence/in_memory.rb +264 -2
- data/lib/phronomy/persistence/repository_facades.rb +221 -2
- data/lib/phronomy/persistence.rb +95 -1
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -0
- 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/version.rb +1 -1
- data/scripts/api_snapshot.rb +3 -3
- data/sig/phronomy/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +28 -32
- data/sig/phronomy/persistence.rbs +64 -3
- metadata +30 -12
- data/lib/phronomy/multi_agent/coordination_state.rb +0 -18
- data/lib/phronomy/multi_agent/coordinator.rb +0 -154
- data/lib/phronomy/multi_agent/runner.rb +0 -98
|
@@ -4,6 +4,7 @@ require_relative "runtime/timer_queue"
|
|
|
4
4
|
require_relative "runtime/shutdown_result"
|
|
5
5
|
require_relative "runtime/timer_service"
|
|
6
6
|
require_relative "runtime/agent_ownership_registry"
|
|
7
|
+
require_relative "runtime/team_ownership_registry"
|
|
7
8
|
|
|
8
9
|
module Phronomy
|
|
9
10
|
class Runtime
|
|
@@ -65,6 +66,7 @@ module Phronomy
|
|
|
65
66
|
)
|
|
66
67
|
@multi_agent_admissions = Phronomy::MultiAgent::AdmissionRegistry.new
|
|
67
68
|
@agent_ownership_registry = AgentOwnershipRegistry.new(runtime: self)
|
|
69
|
+
@team_ownership_registry = TeamOwnershipRegistry.new
|
|
68
70
|
@lifecycle_mutex = Mutex.new
|
|
69
71
|
@shutdown_mutex = Mutex.new
|
|
70
72
|
@state = :running
|
|
@@ -109,6 +111,16 @@ module Phronomy
|
|
|
109
111
|
loop_instance&.agent_execution_owner(execution_id)
|
|
110
112
|
end
|
|
111
113
|
|
|
114
|
+
# @api private
|
|
115
|
+
def __team_owner(id, klass:, create:, persistence:, &block)
|
|
116
|
+
@team_ownership_registry.fetch(id, klass: klass, create: create, persistence: persistence, &block)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @api private
|
|
120
|
+
def __get_team(id, klass:)
|
|
121
|
+
@team_ownership_registry.get(id.to_s, klass: klass)
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
# @api private
|
|
113
125
|
def __create_agent(agent_id, expected_class:, &block)
|
|
114
126
|
@agent_ownership_registry.create(agent_id, expected_class: expected_class, &block)
|
|
@@ -226,9 +238,11 @@ module Phronomy
|
|
|
226
238
|
end
|
|
227
239
|
loop_instance&.begin_draining
|
|
228
240
|
@agent_ownership_registry.begin_draining
|
|
241
|
+
@team_ownership_registry.begin_draining
|
|
229
242
|
|
|
230
243
|
admission_idle = @multi_agent_admissions.wait_until_idle(drain_deadline)
|
|
231
244
|
agent_ownership_stable = @agent_ownership_registry.wait_until_stable(drain_deadline)
|
|
245
|
+
team_ownership_stable = @team_ownership_registry.wait_until_stable(drain_deadline)
|
|
232
246
|
loop_idle = !loop_instance || loop_instance.wait_until_idle(drain_deadline)
|
|
233
247
|
|
|
234
248
|
@lifecycle_mutex.synchronize do
|
|
@@ -243,7 +257,7 @@ module Phronomy
|
|
|
243
257
|
end
|
|
244
258
|
|
|
245
259
|
subsystem_error = shutdown_pools_and_timer
|
|
246
|
-
cleanup_complete = admission_idle && agent_ownership_stable && loop_idle &&
|
|
260
|
+
cleanup_complete = admission_idle && agent_ownership_stable && team_ownership_stable && loop_idle &&
|
|
247
261
|
(!loop_instance || !loop_instance.thread_alive?) &&
|
|
248
262
|
event_loop_status != :cancel_timeout &&
|
|
249
263
|
subsystem_error.nil?
|
|
@@ -256,6 +270,7 @@ module Phronomy
|
|
|
256
270
|
end
|
|
257
271
|
|
|
258
272
|
@agent_ownership_registry.shutdown! if cleanup_complete
|
|
273
|
+
@team_ownership_registry.shutdown! if cleanup_complete
|
|
259
274
|
|
|
260
275
|
result = ShutdownResult.new(
|
|
261
276
|
runtime_outcome: runtime_outcome,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module MultiAgent
|
|
7
|
+
# Agent-owned transactions reserve child work; Task callbacks only wake observers.
|
|
8
|
+
# @api private
|
|
9
|
+
class DurableSubagentCoordinator
|
|
10
|
+
KEY = "multi_agent_coordination_ref"
|
|
11
|
+
|
|
12
|
+
def self.prepare(parent, execution, tx:)
|
|
13
|
+
ref = execution.metadata[KEY]
|
|
14
|
+
snapshot = ref ? tx.contents.fetch_json(ref) : {"kind" => "static_subagent", "children" => []}
|
|
15
|
+
children = snapshot.fetch("children").map(&:dup)
|
|
16
|
+
children.each do |child|
|
|
17
|
+
begin
|
|
18
|
+
exact = tx.executions.load(child.fetch("execution_id"))
|
|
19
|
+
rescue Phronomy::Persistence::NotFoundError
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
raise Phronomy::Persistence::ConflictError, "Child owner mismatch" unless exact.agent_id == child.fetch("agent_id")
|
|
23
|
+
child.merge!("state" => exact.status.to_s, "result_ref" => exact.result_ref, "error_ref" => exact.error_ref)
|
|
24
|
+
end
|
|
25
|
+
Array(execution.metadata[Phronomy::Agent::RecoverySupport::TOOL_BATCH_METADATA_KEY]).each do |tool|
|
|
26
|
+
registration = parent.class.registered_subagents.find { |name, _| "dispatch_to_#{name}" == tool.fetch("tool_name") }
|
|
27
|
+
next unless registration
|
|
28
|
+
next if children.any? { |child| child.fetch("slot") == tool.fetch("tool_invocation_id") }
|
|
29
|
+
name, settings = registration
|
|
30
|
+
definition = settings.fetch(:agent_class).agent_definition
|
|
31
|
+
knowledge = settings.fetch(:inherit_knowledge) ? parent.send(:active_knowledge_snapshot) : []
|
|
32
|
+
children << {
|
|
33
|
+
"slot" => tool.fetch("tool_invocation_id"), "name" => name.to_s,
|
|
34
|
+
"definition" => definition.transform_keys(&:to_s),
|
|
35
|
+
"agent_id" => SecureRandom.uuid, "execution_id" => SecureRandom.uuid,
|
|
36
|
+
"input_ref" => tx.contents.put_text(tool.fetch("arguments").fetch("input")),
|
|
37
|
+
"durable_context_ref" => execution.metadata["durable_context_ref"],
|
|
38
|
+
"knowledge_ref" => tx.contents.put_json(Phronomy::Agent::RecoverySupport.canonical_copy(knowledge)),
|
|
39
|
+
"state" => "reserved", "result_ref" => nil, "error_ref" => nil,
|
|
40
|
+
"on_error" => settings.fetch(:on_error).to_s
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
return execution if children.empty?
|
|
44
|
+
value = tx.contents.put_json(snapshot.merge("children" => children))
|
|
45
|
+
execution.with(execution_revision: execution.execution_revision, metadata: execution.metadata.merge(KEY => value))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.start(parent:, tool_invocation_id:, parent_execution_id:, config:)
|
|
49
|
+
runtime = Phronomy::Runtime.instance
|
|
50
|
+
completion = Phronomy::Task.deferred(name: "durable-subagent:#{tool_invocation_id}")
|
|
51
|
+
preparation = runtime.offload.submit(on_full: :raise) do
|
|
52
|
+
current = parent.persistence.executions.load(parent_execution_id)
|
|
53
|
+
raise Phronomy::Persistence::ConflictError, "Parent owner mismatch" unless current.agent_id == parent.agent_id
|
|
54
|
+
snapshot = parent.persistence.contents.fetch_json(current.metadata.fetch(KEY))
|
|
55
|
+
child = snapshot.fetch("children").find { |entry| entry.fetch("slot") == tool_invocation_id }
|
|
56
|
+
raise Phronomy::ExecutionRehydrationRequiredError, "Missing reserved child slot" unless child
|
|
57
|
+
definition = parent.class.registered_subagents.find { |name, _| name.to_s == child.fetch("name") }&.last
|
|
58
|
+
unless definition && definition.fetch(:agent_class).agent_definition.transform_keys(&:to_s) == child.fetch("definition")
|
|
59
|
+
raise Phronomy::ConfigurationError, "Registered child definition changed: #{child.fetch("name")}"
|
|
60
|
+
end
|
|
61
|
+
klass = definition.fetch(:agent_class)
|
|
62
|
+
id = child.fetch("agent_id")
|
|
63
|
+
agent = klass.get(id)
|
|
64
|
+
unless agent
|
|
65
|
+
begin
|
|
66
|
+
parent.persistence.agents.load(id)
|
|
67
|
+
exists = true
|
|
68
|
+
rescue Phronomy::Persistence::NotFoundError
|
|
69
|
+
exists = false
|
|
70
|
+
end
|
|
71
|
+
listener = parent.send(:_phronomy_event_listener)
|
|
72
|
+
agent = if exists
|
|
73
|
+
klass.load(id, persistence: parent.persistence, on_event: listener)
|
|
74
|
+
else
|
|
75
|
+
knowledge = parent.persistence.contents.fetch_json(child.fetch("knowledge_ref"))
|
|
76
|
+
context = Phronomy::Agent::ContextImporter::ImportedContext.new(records: knowledge.map do |item|
|
|
77
|
+
Phronomy::Agent::ContextImporter::ImportedRecord.new(kind: :knowledge,
|
|
78
|
+
channel: :context, role: :user, content: item.fetch("content"),
|
|
79
|
+
content_format: :text, metadata: item.fetch("metadata"))
|
|
80
|
+
end)
|
|
81
|
+
klass.create(agent_id: id, persistence: parent.persistence,
|
|
82
|
+
context: context, on_event: listener)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
unless agent.persistence.equal?(parent.persistence)
|
|
86
|
+
raise Phronomy::ConfigurationError, "Child Persistence instance mismatch"
|
|
87
|
+
end
|
|
88
|
+
durable_context = child["durable_context_ref"] && parent.persistence.contents.fetch_json(child["durable_context_ref"])
|
|
89
|
+
[child, agent, parent.persistence.contents.fetch_text(child.fetch("input_ref")), durable_context]
|
|
90
|
+
end
|
|
91
|
+
preparation.on_complete do |prepared, failure|
|
|
92
|
+
if failure
|
|
93
|
+
completion.fail(Phronomy::ExecutionRehydrationRequiredError.new("Child reservation requires recovery: #{failure.message}"))
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
child, agent, input, durable_context = prepared
|
|
97
|
+
child_config = {cancellation_token: config[:cancellation_token] || Phronomy::Concurrency::CancellationToken.new,
|
|
98
|
+
invocation_context: config[:invocation_context],
|
|
99
|
+
phronomy_coordination: {"kind" => "subagent", "parent_execution_id" => parent_execution_id,
|
|
100
|
+
"parent_agent_id" => parent.agent_id, "slot" => tool_invocation_id}}.compact
|
|
101
|
+
child_config = child_config.merge(durable_context: durable_context) if child["durable_context_ref"]
|
|
102
|
+
source = Phronomy::Agent::ExactExecution.start(agent: agent,
|
|
103
|
+
execution_id: child.fetch("execution_id"), input: input, config: child_config)
|
|
104
|
+
source.on_complete do |result, error|
|
|
105
|
+
if error
|
|
106
|
+
if error.is_a?(Phronomy::CancellationError)
|
|
107
|
+
completion.fail(error)
|
|
108
|
+
else
|
|
109
|
+
completion.fail(Phronomy::ExecutionRehydrationRequiredError.new(
|
|
110
|
+
"Child #{child.fetch("execution_id")} is unfinished: #{error.message}"
|
|
111
|
+
))
|
|
112
|
+
end
|
|
113
|
+
elsif result[:error]
|
|
114
|
+
if child.fetch("on_error") == "skip"
|
|
115
|
+
completion.complete(nil)
|
|
116
|
+
else
|
|
117
|
+
completion.fail(Phronomy::Agent::RecoverySupport.error_from_failure(result[:error]))
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
completion.complete(result[:output])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
rescue => error
|
|
124
|
+
completion.fail(Phronomy::ExecutionRehydrationRequiredError.new("Child coordination requires recovery: #{error.message}"))
|
|
125
|
+
end
|
|
126
|
+
completion
|
|
127
|
+
rescue => error
|
|
128
|
+
completion ||= Phronomy::Task.deferred(name: "durable-subagent")
|
|
129
|
+
completion.fail(error)
|
|
130
|
+
completion
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
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,20 +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
|
-
config: task_config
|
|
54
|
-
)
|
|
89
|
+
source = agent.invoke_async(input, config: task_config)
|
|
55
90
|
result_task = Phronomy::Task.deferred(
|
|
56
91
|
name: "subagent-tool-#{name}"
|
|
57
92
|
)
|
|
@@ -213,6 +248,17 @@ module Phronomy
|
|
|
213
248
|
).wait_result
|
|
214
249
|
end
|
|
215
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
|
+
|
|
216
262
|
private
|
|
217
263
|
|
|
218
264
|
def prepare_tool_class(tool_class, invocation: nil)
|
|
@@ -225,8 +271,10 @@ module Phronomy
|
|
|
225
271
|
end&.last
|
|
226
272
|
inherits_knowledge = registration ? registration.fetch(:inherit_knowledge, true) : true
|
|
227
273
|
|
|
228
|
-
captured_context = {}
|
|
229
|
-
|
|
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
|
|
230
278
|
if invocation
|
|
231
279
|
captured_context[:config] = invocation.config
|
|
232
280
|
captured_context[:invocation_context] = invocation.config[:invocation_context]
|