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
data/lib/phronomy/agent/base.rb
CHANGED
|
@@ -40,17 +40,6 @@ module Phronomy
|
|
|
40
40
|
private_constant :APPROVAL_CONFIGURATION_INIT_MUTEX
|
|
41
41
|
|
|
42
42
|
class << self
|
|
43
|
-
# Sets or reads the LLM model identifier for this agent.
|
|
44
|
-
# When called without an argument, returns the stored model or the
|
|
45
|
-
# global default from {Phronomy.configuration}.
|
|
46
|
-
#
|
|
47
|
-
# @param name [String, nil] model identifier (e.g. "gpt-4o", "claude-3-5-sonnet")
|
|
48
|
-
# @return [String, nil] the model name when used as a reader
|
|
49
|
-
# @example
|
|
50
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
51
|
-
# model "gpt-4o"
|
|
52
|
-
# end
|
|
53
|
-
# @api public
|
|
54
43
|
def model(name = nil)
|
|
55
44
|
if name
|
|
56
45
|
@model = name
|
|
@@ -59,22 +48,6 @@ module Phronomy
|
|
|
59
48
|
end
|
|
60
49
|
end
|
|
61
50
|
|
|
62
|
-
# Sets or reads the system instructions for this agent.
|
|
63
|
-
# Accepts a String, a {Phronomy::Agent::Context::Instruction::PromptTemplate}, or a block (Proc).
|
|
64
|
-
# When used as a reader (no argument, no block), returns the stored value.
|
|
65
|
-
#
|
|
66
|
-
# @param text [String, Phronomy::Agent::Context::Instruction::PromptTemplate, nil]
|
|
67
|
-
# @yield optionally provide instructions as a block
|
|
68
|
-
# @return [String, Phronomy::Agent::Context::Instruction::PromptTemplate, Proc, nil]
|
|
69
|
-
# @example String instructions
|
|
70
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
71
|
-
# instructions "You are a helpful assistant."
|
|
72
|
-
# end
|
|
73
|
-
# @example Block instructions
|
|
74
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
75
|
-
# instructions { |input| "Answer in #{input[:lang]}." }
|
|
76
|
-
# end
|
|
77
|
-
# @api public
|
|
78
51
|
def instructions(text = nil, &block)
|
|
79
52
|
if text || block_given?
|
|
80
53
|
@instructions = text || block
|
|
@@ -84,19 +57,6 @@ module Phronomy
|
|
|
84
57
|
end
|
|
85
58
|
end
|
|
86
59
|
|
|
87
|
-
# Registers tool classes for this agent.
|
|
88
|
-
#
|
|
89
|
-
# The setter accepts one Hash mapping each Tool class to an explicit alias
|
|
90
|
-
# name (String) or nil (use the Tool's own name). Calling without an
|
|
91
|
-
# argument returns the registered Tool classes.
|
|
92
|
-
#
|
|
93
|
-
# @example
|
|
94
|
-
# tools(
|
|
95
|
-
# Weather::SearchTool => "weather_search",
|
|
96
|
-
# Places::SearchTool => "places_search",
|
|
97
|
-
# CurrentTimeTool => nil
|
|
98
|
-
# )
|
|
99
|
-
# @api public
|
|
100
60
|
def tools(definitions = nil)
|
|
101
61
|
if definitions.nil?
|
|
102
62
|
return @tools if instance_variable_defined?(:@tools)
|
|
@@ -113,11 +73,6 @@ module Phronomy
|
|
|
113
73
|
.reject { |_, value| value.nil? }
|
|
114
74
|
end
|
|
115
75
|
|
|
116
|
-
# Returns the alias map registered via .tools.
|
|
117
|
-
# Merges parent class aliases so subclasses inherit their parent's mappings.
|
|
118
|
-
# Subclass-specific aliases take precedence over parent aliases.
|
|
119
|
-
# @return [Hash{Class => String}]
|
|
120
|
-
# @api public
|
|
121
76
|
def tool_aliases
|
|
122
77
|
own = @tool_aliases || {}
|
|
123
78
|
if superclass.respond_to?(:tool_aliases)
|
|
@@ -127,18 +82,6 @@ module Phronomy
|
|
|
127
82
|
end
|
|
128
83
|
end
|
|
129
84
|
|
|
130
|
-
# Sets or reads the LLM provider for this agent.
|
|
131
|
-
# Required when using a model not registered in RubyLLM's model registry
|
|
132
|
-
# (e.g. locally-hosted models via LM Studio or Ollama).
|
|
133
|
-
#
|
|
134
|
-
# @param name [Symbol, nil] e.g. +:openai+, +:anthropic+, +:ollama+
|
|
135
|
-
# @return [Symbol, nil]
|
|
136
|
-
# @example
|
|
137
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
138
|
-
# model "openai/gpt-oss-20b"
|
|
139
|
-
# provider :openai
|
|
140
|
-
# end
|
|
141
|
-
# @api public
|
|
142
85
|
def provider(name = nil)
|
|
143
86
|
if name
|
|
144
87
|
@provider = name
|
|
@@ -148,16 +91,6 @@ module Phronomy
|
|
|
148
91
|
end
|
|
149
92
|
end
|
|
150
93
|
|
|
151
|
-
# Sets or reads the sampling temperature sent to the LLM.
|
|
152
|
-
# When nil, the provider's default is used.
|
|
153
|
-
#
|
|
154
|
-
# @param val [Float, nil] temperature (0.0 to 2.0 depending on provider)
|
|
155
|
-
# @return [Float, nil]
|
|
156
|
-
# @example
|
|
157
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
158
|
-
# temperature 0.2
|
|
159
|
-
# end
|
|
160
|
-
# @api public
|
|
161
94
|
def temperature(val = nil)
|
|
162
95
|
if val
|
|
163
96
|
@temperature = val
|
|
@@ -166,16 +99,6 @@ module Phronomy
|
|
|
166
99
|
end
|
|
167
100
|
end
|
|
168
101
|
|
|
169
|
-
# Sets or reads the maximum number of LLM call cycles for ReAct agents.
|
|
170
|
-
# Each tool call and follow-up counts as one iteration. Defaults to 10.
|
|
171
|
-
#
|
|
172
|
-
# @param val [Integer, nil]
|
|
173
|
-
# @return [Integer]
|
|
174
|
-
# @example
|
|
175
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
176
|
-
# max_iterations 5
|
|
177
|
-
# end
|
|
178
|
-
# @api public
|
|
179
102
|
def max_iterations(val = nil)
|
|
180
103
|
if val
|
|
181
104
|
@max_iterations = val
|
|
@@ -184,10 +107,6 @@ module Phronomy
|
|
|
184
107
|
end
|
|
185
108
|
end
|
|
186
109
|
|
|
187
|
-
# When enabled, attaches Anthropic prompt-cache markers to the system
|
|
188
|
-
# message so that the fixed instructions are served from cache on
|
|
189
|
-
# subsequent turns, reducing input-token costs.
|
|
190
|
-
# @api public
|
|
191
110
|
def cache_instructions(enabled = nil)
|
|
192
111
|
if enabled.nil?
|
|
193
112
|
@cache_instructions
|
|
@@ -196,9 +115,6 @@ module Phronomy
|
|
|
196
115
|
end
|
|
197
116
|
end
|
|
198
117
|
|
|
199
|
-
# Tokens to reserve for the model's output.
|
|
200
|
-
# When nil, the model's max_output_tokens from the registry is used.
|
|
201
|
-
# @api public
|
|
202
118
|
def max_output_tokens(val = nil)
|
|
203
119
|
if val.nil?
|
|
204
120
|
@max_output_tokens
|
|
@@ -207,8 +123,6 @@ module Phronomy
|
|
|
207
123
|
end
|
|
208
124
|
end
|
|
209
125
|
|
|
210
|
-
# Overrides the context window size used for token budget calculations.
|
|
211
|
-
# @api public
|
|
212
126
|
def context_window(val = nil)
|
|
213
127
|
if val.nil?
|
|
214
128
|
@context_window
|
|
@@ -217,105 +131,238 @@ module Phronomy
|
|
|
217
131
|
end
|
|
218
132
|
end
|
|
219
133
|
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
@
|
|
134
|
+
# Binds one Application-constructed ContextPolicy instance to this
|
|
135
|
+
# Agent class. Policy binding is Application code/runtime wiring rather
|
|
136
|
+
# than durable Agent state.
|
|
137
|
+
def context_policy(*args)
|
|
138
|
+
if args.empty?
|
|
139
|
+
return @context_policy if instance_variable_defined?(:@context_policy)
|
|
140
|
+
return superclass.context_policy if superclass.respond_to?(:context_policy)
|
|
141
|
+
return ContextPolicies::Default.instance
|
|
226
142
|
end
|
|
227
|
-
|
|
143
|
+
unless args.length == 1 && args.first.is_a?(ContextPolicy)
|
|
144
|
+
raise ArgumentError,
|
|
145
|
+
"context_policy expects one Phronomy::Agent::ContextPolicy instance"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@context_policy = args.first
|
|
149
|
+
end
|
|
228
150
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
151
|
+
def agent_definition(id: nil, version: nil)
|
|
152
|
+
if !id.nil? || !version.nil?
|
|
153
|
+
raise ArgumentError, "agent_definition requires version:" if version.nil?
|
|
154
|
+
|
|
155
|
+
definition_id = id || name
|
|
156
|
+
unless definition_id
|
|
157
|
+
raise Phronomy::ConfigurationError,
|
|
158
|
+
"anonymous Agent class must declare agent_definition id: ..., version: ..."
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@agent_definition = {
|
|
162
|
+
id: definition_id.to_s.freeze,
|
|
163
|
+
version: Integer(version)
|
|
164
|
+
}.freeze
|
|
235
165
|
end
|
|
166
|
+
return @agent_definition if @agent_definition
|
|
236
167
|
|
|
237
168
|
raise Phronomy::ConfigurationError,
|
|
238
|
-
"#{name || self} must declare agent_definition
|
|
169
|
+
"#{name || self} must declare agent_definition version: ..."
|
|
239
170
|
end
|
|
240
171
|
|
|
241
|
-
def create(
|
|
172
|
+
def create(
|
|
173
|
+
agent_id: SecureRandom.uuid,
|
|
174
|
+
context: nil,
|
|
175
|
+
knowledge: [],
|
|
176
|
+
persistence: nil,
|
|
177
|
+
metadata: {},
|
|
178
|
+
on_event: nil,
|
|
179
|
+
&event_block
|
|
180
|
+
)
|
|
242
181
|
new(
|
|
243
182
|
agent_id: agent_id,
|
|
244
183
|
context: context,
|
|
245
184
|
knowledge: knowledge,
|
|
246
185
|
persistence: persistence,
|
|
247
|
-
metadata: metadata
|
|
186
|
+
metadata: metadata,
|
|
187
|
+
on_event: on_event,
|
|
188
|
+
&event_block
|
|
248
189
|
)
|
|
249
190
|
end
|
|
250
191
|
|
|
251
|
-
|
|
252
|
-
|
|
192
|
+
# Resolves one existing logical Agent. A live process-local owner wins
|
|
193
|
+
# without a Persistence reload; otherwise the durable Agent is hydrated.
|
|
194
|
+
def load(agent_id, persistence:, on_event: nil, &event_block)
|
|
195
|
+
raise ArgumentError, "persistence is required" unless persistence
|
|
196
|
+
if on_event && event_block
|
|
197
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
key = agent_id.to_s
|
|
201
|
+
raise ArgumentError, "agent_id must not be empty" if key.empty?
|
|
202
|
+
|
|
203
|
+
listener_supplied = !on_event.nil? || !event_block.nil?
|
|
204
|
+
runtime = Phronomy::Runtime.instance
|
|
205
|
+
materialized = false
|
|
206
|
+
|
|
207
|
+
agent = runtime.__load_agent(key, expected_class: self) do |owner_runtime|
|
|
208
|
+
materialized = true
|
|
209
|
+
instance = __construct_owned_agent(
|
|
210
|
+
owner_runtime,
|
|
211
|
+
key,
|
|
212
|
+
agent_id: key,
|
|
213
|
+
persistence: persistence,
|
|
214
|
+
load_existing: true,
|
|
215
|
+
on_event: on_event,
|
|
216
|
+
&event_block
|
|
217
|
+
)
|
|
218
|
+
Phronomy::Agent::RecoveryCoordinator.new(instance).recover_on_load!
|
|
219
|
+
instance
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
unless agent.persistence.equal?(persistence)
|
|
223
|
+
raise Phronomy::ConfigurationError,
|
|
224
|
+
"Agent #{key.inspect} is already live with a different Persistence instance"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if !materialized && listener_supplied
|
|
228
|
+
raise Phronomy::ConfigurationError,
|
|
229
|
+
"Agent #{key.inspect} is already live; load cannot add, replace, or re-bind on_event"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
agent
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns only the process-local live Agent owner. Does not access
|
|
236
|
+
# Persistence and returns nil when this Runtime has no live owner.
|
|
237
|
+
def get(agent_id)
|
|
238
|
+
Phronomy::Runtime.instance.__get_agent(agent_id, expected_class: self)
|
|
253
239
|
end
|
|
254
240
|
|
|
255
241
|
# Resolves the live Agent instance that currently owns execution_id in
|
|
256
|
-
# this process.
|
|
242
|
+
# this process. The Runtime returns only a read-only ownership view;
|
|
243
|
+
# mutable Agent execution state remains EventLoop-owned.
|
|
257
244
|
def live_for_execution(execution_id)
|
|
258
|
-
|
|
259
|
-
unless
|
|
245
|
+
owner = Phronomy::Runtime.instance.__agent_execution_owner(execution_id)
|
|
246
|
+
unless owner
|
|
260
247
|
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
261
|
-
"no live
|
|
248
|
+
"no live execution owner for #{execution_id}; durable rehydration is required"
|
|
262
249
|
end
|
|
263
250
|
|
|
264
|
-
agent =
|
|
251
|
+
agent = owner.agent
|
|
265
252
|
unless agent.is_a?(self)
|
|
266
253
|
raise ArgumentError,
|
|
267
|
-
"live
|
|
254
|
+
"live execution #{execution_id} belongs to #{agent.class}, not #{self}"
|
|
268
255
|
end
|
|
269
256
|
|
|
270
257
|
agent
|
|
271
258
|
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def __construct_owned_agent(owner_runtime, reserved_agent_id, *args, **kwargs, &block)
|
|
263
|
+
instance = allocate
|
|
264
|
+
instance.send(:__prepare_runtime_owner!, owner_runtime, reserved_agent_id)
|
|
265
|
+
instance.send(:initialize, *args, **kwargs, &block)
|
|
266
|
+
instance
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @api private
|
|
271
|
+
def __coordination_config
|
|
272
|
+
@_phronomy_coordination_config || {}.freeze
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Framework-owned idempotent Tool operations opt into exact replay.
|
|
276
|
+
# @api private
|
|
277
|
+
def __framework_tool_replayable?(_name) = false
|
|
278
|
+
|
|
279
|
+
# @api private
|
|
280
|
+
def __framework_call?(name)
|
|
281
|
+
__framework_tool_replayable?(name) || Array(__coordination_config[:phronomy_handoff_bindings]).any? { |binding| binding.tool_name == name }
|
|
272
282
|
end
|
|
273
283
|
|
|
284
|
+
# Captured Tool wiring is supplied explicitly; this hook only writes semantic values.
|
|
285
|
+
# @api private
|
|
286
|
+
def __prepare_coordination_record(execution, tx:) = execution
|
|
287
|
+
|
|
288
|
+
# Runtime configuration only; no value returned here is serialized.
|
|
289
|
+
# @api private
|
|
290
|
+
def __invocation_config(config) = config
|
|
291
|
+
|
|
274
292
|
attr_reader :agent_id, :persistence
|
|
275
293
|
|
|
276
294
|
def initialize(
|
|
277
|
-
agent_id:
|
|
295
|
+
agent_id: nil,
|
|
278
296
|
context: nil,
|
|
279
297
|
knowledge: [],
|
|
280
298
|
persistence: nil,
|
|
281
299
|
metadata: {},
|
|
282
|
-
load_existing: false
|
|
300
|
+
load_existing: false,
|
|
301
|
+
on_event: nil,
|
|
302
|
+
&event_block
|
|
283
303
|
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@
|
|
304
|
+
if on_event && event_block
|
|
305
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
306
|
+
end
|
|
307
|
+
@_phronomy_event_listener = on_event || event_block
|
|
288
308
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
root = tx.agents.load(@agent_id)
|
|
293
|
-
records = tx.journals.read(
|
|
294
|
-
@agent_id,
|
|
295
|
-
limit: root.journal_position
|
|
296
|
-
)
|
|
297
|
-
end
|
|
298
|
-
validate_loaded_definition!(root)
|
|
299
|
-
@root = root
|
|
300
|
-
@_phronomy_journal_records = Array(records).dup.freeze
|
|
309
|
+
reserved_agent_id = @_phronomy_reserved_agent_id
|
|
310
|
+
effective_agent_id = if agent_id.nil?
|
|
311
|
+
reserved_agent_id || SecureRandom.uuid.to_s
|
|
301
312
|
else
|
|
302
|
-
|
|
313
|
+
agent_id.to_s
|
|
314
|
+
end
|
|
315
|
+
raise ArgumentError, "agent_id must not be empty" if effective_agent_id.empty?
|
|
316
|
+
|
|
317
|
+
if @_phronomy_runtime_owner_state == :constructing
|
|
318
|
+
unless reserved_agent_id == effective_agent_id
|
|
319
|
+
raise Phronomy::Error,
|
|
320
|
+
"Agent initializer changed reserved identity from " \
|
|
321
|
+
"#{reserved_agent_id.inspect} to #{effective_agent_id.inspect}"
|
|
322
|
+
end
|
|
323
|
+
initialize_owned_state(
|
|
324
|
+
agent_id: effective_agent_id,
|
|
303
325
|
context: context,
|
|
304
326
|
knowledge: knowledge,
|
|
305
|
-
|
|
327
|
+
persistence: persistence,
|
|
328
|
+
metadata: metadata,
|
|
329
|
+
load_existing: load_existing
|
|
306
330
|
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
331
|
+
return
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if load_existing
|
|
335
|
+
raise ArgumentError,
|
|
336
|
+
"load_existing: is an internal hydration option; use .load(agent_id, persistence:)"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
runtime = Phronomy::Runtime.instance
|
|
340
|
+
begin
|
|
341
|
+
runtime.__create_agent(effective_agent_id, expected_class: self.class) do |owner_runtime|
|
|
342
|
+
__prepare_runtime_owner!(owner_runtime, effective_agent_id)
|
|
343
|
+
initialize_owned_state(
|
|
344
|
+
agent_id: effective_agent_id,
|
|
345
|
+
context: context,
|
|
346
|
+
knowledge: knowledge,
|
|
347
|
+
persistence: persistence,
|
|
348
|
+
metadata: metadata,
|
|
349
|
+
load_existing: false
|
|
350
|
+
)
|
|
351
|
+
self
|
|
352
|
+
end
|
|
353
|
+
rescue Phronomy::Persistence::ConflictError => error
|
|
354
|
+
raise Phronomy::AgentAlreadyExistsError,
|
|
355
|
+
"Agent #{effective_agent_id.inspect} already exists durably: #{error.message}"
|
|
311
356
|
end
|
|
312
357
|
end
|
|
313
358
|
|
|
314
359
|
def agent_root
|
|
360
|
+
__assert_agent_accessible!
|
|
315
361
|
@root
|
|
316
362
|
end
|
|
317
363
|
|
|
318
364
|
def journal_projection
|
|
365
|
+
__assert_agent_accessible!
|
|
319
366
|
Agent::JournalProjection.new(
|
|
320
367
|
agent_root: @root,
|
|
321
368
|
records: _journal_records_snapshot
|
|
@@ -336,8 +383,6 @@ module Phronomy
|
|
|
336
383
|
end
|
|
337
384
|
end
|
|
338
385
|
|
|
339
|
-
# Logically clears all persistent Knowledge registered before this point.
|
|
340
|
-
# Raw Journal records remain append-only and are not deleted.
|
|
341
386
|
def clear_knowledge!
|
|
342
387
|
mutate_context!(:knowledge_cleared) do |root|
|
|
343
388
|
root.with(
|
|
@@ -347,9 +392,8 @@ module Phronomy
|
|
|
347
392
|
end
|
|
348
393
|
end
|
|
349
394
|
|
|
350
|
-
# Appends persistent Knowledge to the Agent Journal. The live Agent owns the
|
|
351
|
-
# current logical root/Journal view; Persistence is advanced optimistically.
|
|
352
395
|
def add_knowledge(content, metadata: {})
|
|
396
|
+
__assert_live_agent!
|
|
353
397
|
current = agent_root
|
|
354
398
|
next_root = nil
|
|
355
399
|
appended = nil
|
|
@@ -402,41 +446,194 @@ module Phronomy
|
|
|
402
446
|
end
|
|
403
447
|
|
|
404
448
|
def purge!
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
449
|
+
return true if @_phronomy_runtime_owner_state == :purged
|
|
450
|
+
|
|
451
|
+
__assert_live_agent!
|
|
452
|
+
runtime = @_phronomy_runtime_owner
|
|
453
|
+
token = runtime.__begin_agent_purge(self)
|
|
454
|
+
if runtime.__agent_execution_admitted?(agent_id)
|
|
455
|
+
runtime.__abort_agent_purge(self, token)
|
|
456
|
+
raise Phronomy::AgentBusyError,
|
|
457
|
+
"Agent #{agent_id.inspect} has a nonterminal top-level execution"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
begin
|
|
461
|
+
persistence.transaction do |tx|
|
|
462
|
+
tx.executions.assert_idle!(agent_id)
|
|
463
|
+
if (routing = tx.handoff_states.load(agent_id))
|
|
464
|
+
unless routing.phase == "stable" && tx.executions.list_active(routing.active_agent_id).empty?
|
|
465
|
+
raise Phronomy::AgentBusyError, "Handoff anchor #{agent_id} owns an unfinished turn"
|
|
466
|
+
end
|
|
467
|
+
tx.handoff_states.delete(agent_id, expected_revision: routing.handoff_revision)
|
|
468
|
+
end
|
|
469
|
+
tx.journals.delete(agent_id)
|
|
470
|
+
tx.executions.delete_for_agent(agent_id)
|
|
471
|
+
tx.agents.delete(agent_id)
|
|
472
|
+
end
|
|
473
|
+
rescue Phronomy::AgentBusyError,
|
|
474
|
+
Phronomy::Persistence::ConflictError,
|
|
475
|
+
Phronomy::Persistence::NotFoundError,
|
|
476
|
+
Phronomy::Persistence::SerializationError,
|
|
477
|
+
ArgumentError
|
|
478
|
+
runtime.__abort_agent_purge(self, token)
|
|
479
|
+
raise
|
|
480
|
+
rescue
|
|
481
|
+
# Without F1 reconciliation support we cannot infer that a failed
|
|
482
|
+
# durable delete did not commit. Keep the identity fail-closed.
|
|
483
|
+
runtime.__leave_agent_purge_uncertain(self, token)
|
|
484
|
+
raise
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
runtime.__complete_agent_purge(self, token)
|
|
488
|
+
true
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Runtime ownership lifecycle hooks. These are internal coordination
|
|
492
|
+
# methods; application code must use new/create/load/get/purge!.
|
|
493
|
+
private
|
|
494
|
+
|
|
495
|
+
# @api private
|
|
496
|
+
def __prepare_runtime_owner!(runtime, reserved_agent_id)
|
|
497
|
+
@_phronomy_runtime_owner = runtime
|
|
498
|
+
@_phronomy_runtime_owner_state = :constructing
|
|
499
|
+
@_phronomy_reserved_agent_id = reserved_agent_id.to_s.freeze
|
|
500
|
+
self
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# @api private
|
|
504
|
+
def __bind_runtime_owner!(runtime)
|
|
505
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
506
|
+
@_phronomy_runtime_owner_state == :constructing
|
|
507
|
+
raise Phronomy::Error, "Agent Runtime ownership construction state is invalid"
|
|
508
|
+
end
|
|
509
|
+
@_phronomy_runtime_owner_state = :live
|
|
510
|
+
remove_instance_variable(:@_phronomy_reserved_agent_id) if
|
|
511
|
+
instance_variable_defined?(:@_phronomy_reserved_agent_id)
|
|
512
|
+
self
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# @api private
|
|
516
|
+
def __mark_purging!(runtime)
|
|
517
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
518
|
+
@_phronomy_runtime_owner_state == :live
|
|
519
|
+
raise Phronomy::Error, "Agent Runtime ownership purge state is invalid"
|
|
520
|
+
end
|
|
521
|
+
@_phronomy_runtime_owner_state = :purging
|
|
522
|
+
self
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# @api private
|
|
526
|
+
def __restore_live_after_purge_abort!(runtime)
|
|
527
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
528
|
+
@_phronomy_runtime_owner_state == :purging
|
|
529
|
+
raise Phronomy::Error, "Agent Runtime ownership purge rollback state is invalid"
|
|
530
|
+
end
|
|
531
|
+
@_phronomy_runtime_owner_state = :live
|
|
532
|
+
self
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# @api private
|
|
536
|
+
def __mark_ownership_recovery_required!(runtime)
|
|
537
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
538
|
+
%i[constructing purging].include?(@_phronomy_runtime_owner_state)
|
|
539
|
+
raise Phronomy::Error, "Agent Runtime ownership recovery state is invalid"
|
|
540
|
+
end
|
|
541
|
+
@_phronomy_runtime_owner_state = :recovery_required
|
|
542
|
+
self
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# @api private
|
|
546
|
+
def __mark_purged!(runtime)
|
|
547
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
548
|
+
@_phronomy_runtime_owner_state == :purging
|
|
549
|
+
raise Phronomy::Error, "Agent Runtime ownership purge completion state is invalid"
|
|
410
550
|
end
|
|
411
551
|
@root = nil
|
|
412
552
|
@_phronomy_journal_records = [].freeze
|
|
413
|
-
|
|
553
|
+
@_phronomy_runtime_owner_state = :purged
|
|
554
|
+
self
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# @api private
|
|
558
|
+
def __release_runtime_owner!(runtime)
|
|
559
|
+
return self if @_phronomy_runtime_owner_state == :purged
|
|
560
|
+
return self unless @_phronomy_runtime_owner.equal?(runtime)
|
|
561
|
+
|
|
562
|
+
@_phronomy_runtime_owner_state = :released
|
|
563
|
+
self
|
|
414
564
|
end
|
|
415
565
|
|
|
416
|
-
|
|
566
|
+
public
|
|
567
|
+
|
|
568
|
+
# Internal EventLoop apply hook used only after a successful durable
|
|
569
|
+
# operation result has been validated by ExecutionCoordinator.
|
|
417
570
|
def __replace_root(root)
|
|
418
571
|
@root = root
|
|
419
572
|
end
|
|
420
573
|
|
|
421
574
|
private
|
|
422
575
|
|
|
576
|
+
attr_reader :_phronomy_event_listener
|
|
577
|
+
|
|
578
|
+
def initialize_owned_state(
|
|
579
|
+
agent_id:,
|
|
580
|
+
context:,
|
|
581
|
+
knowledge:,
|
|
582
|
+
persistence:,
|
|
583
|
+
metadata:,
|
|
584
|
+
load_existing:
|
|
585
|
+
)
|
|
586
|
+
@persistence = persistence ||
|
|
587
|
+
Phronomy.configuration.persistence ||
|
|
588
|
+
Phronomy::Persistence::InMemory.new
|
|
589
|
+
@agent_id = agent_id.to_s.freeze
|
|
590
|
+
|
|
591
|
+
if load_existing
|
|
592
|
+
root = records = nil
|
|
593
|
+
@persistence.transaction do |tx|
|
|
594
|
+
root = tx.agents.load(@agent_id)
|
|
595
|
+
records = tx.journals.read(
|
|
596
|
+
@agent_id,
|
|
597
|
+
limit: root.journal_position
|
|
598
|
+
)
|
|
599
|
+
end
|
|
600
|
+
validate_loaded_definition!(root)
|
|
601
|
+
@root = root
|
|
602
|
+
@_phronomy_journal_records = Array(records).dup.freeze
|
|
603
|
+
else
|
|
604
|
+
@root = create_agent_root!(
|
|
605
|
+
context: context,
|
|
606
|
+
knowledge: knowledge,
|
|
607
|
+
metadata: metadata
|
|
608
|
+
)
|
|
609
|
+
@_phronomy_journal_records = @persistence.journals.read(
|
|
610
|
+
@agent_id,
|
|
611
|
+
limit: @root.journal_position
|
|
612
|
+
).dup.freeze
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
423
616
|
def validate_loaded_definition!(loaded)
|
|
424
617
|
definition = self.class.agent_definition
|
|
425
|
-
return if loaded
|
|
426
|
-
loaded.definition_version == definition.fetch(:version)
|
|
618
|
+
return if current_definition_compatible?(loaded, definition)
|
|
427
619
|
|
|
428
620
|
raise Phronomy::ConfigurationError,
|
|
429
621
|
"Agent definition mismatch for #{@agent_id}: stored " \
|
|
430
|
-
"#{loaded.agent_definition_id}@#{loaded.
|
|
622
|
+
"#{loaded.agent_definition_id}@#{loaded.agent_definition_version}, runtime " \
|
|
431
623
|
"#{definition.fetch(:id)}@#{definition.fetch(:version)}"
|
|
432
624
|
end
|
|
433
625
|
|
|
626
|
+
def current_definition_compatible?(loaded, runtime_definition)
|
|
627
|
+
loaded.agent_definition_id == runtime_definition.fetch(:id) &&
|
|
628
|
+
loaded.agent_definition_version == runtime_definition.fetch(:version)
|
|
629
|
+
end
|
|
630
|
+
|
|
434
631
|
def create_agent_root!(context:, knowledge:, metadata:)
|
|
435
632
|
definition = self.class.agent_definition
|
|
436
633
|
root = Agent::AgentRoot.create(
|
|
437
634
|
agent_id: agent_id,
|
|
438
635
|
agent_definition_id: definition.fetch(:id),
|
|
439
|
-
|
|
636
|
+
agent_definition_version: definition.fetch(:version),
|
|
440
637
|
metadata: metadata
|
|
441
638
|
)
|
|
442
639
|
persistence.transaction do |tx|
|
|
@@ -507,6 +704,7 @@ module Phronomy
|
|
|
507
704
|
end
|
|
508
705
|
|
|
509
706
|
def mutate_context!(kind, context_affecting: true)
|
|
707
|
+
__assert_live_agent!
|
|
510
708
|
current = agent_root
|
|
511
709
|
next_root = nil
|
|
512
710
|
appended = nil
|
|
@@ -541,7 +739,8 @@ module Phronomy
|
|
|
541
739
|
end
|
|
542
740
|
|
|
543
741
|
def yield_context_revision(current, proposed)
|
|
544
|
-
(proposed.context_revision == current.context_revision) ?
|
|
742
|
+
(proposed.context_revision == current.context_revision) ?
|
|
743
|
+
current.context_revision + 1 : proposed.context_revision
|
|
545
744
|
end
|
|
546
745
|
|
|
547
746
|
def _journal_records_snapshot
|
|
@@ -558,44 +757,73 @@ module Phronomy
|
|
|
558
757
|
|
|
559
758
|
public
|
|
560
759
|
|
|
561
|
-
def _add_handoff_tool(tool_class)
|
|
562
|
-
@_handoff_tools ||= []
|
|
563
|
-
@_handoff_tools << tool_class
|
|
564
|
-
self
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
def _handoff_tools
|
|
568
|
-
@_handoff_tools || []
|
|
569
|
-
end
|
|
570
|
-
|
|
571
760
|
def tool_approval_policy(&block)
|
|
761
|
+
__assert_live_agent!
|
|
572
762
|
raise ArgumentError, "tool_approval_policy requires a block" unless block
|
|
573
763
|
|
|
574
764
|
_approval_configuration_mutex.synchronize { @tool_approval_policy = block }
|
|
575
765
|
self
|
|
576
766
|
end
|
|
577
767
|
|
|
578
|
-
|
|
579
|
-
raise ArgumentError, "on_tool_approval_required requires a block" unless block
|
|
768
|
+
private
|
|
580
769
|
|
|
581
|
-
|
|
582
|
-
|
|
770
|
+
def __assert_agent_accessible!
|
|
771
|
+
case @_phronomy_runtime_owner_state
|
|
772
|
+
when :constructing, :live
|
|
773
|
+
true
|
|
774
|
+
when :purged
|
|
775
|
+
raise Phronomy::AgentPurgedError, "Agent #{agent_id.inspect} has been purged"
|
|
776
|
+
when :purging
|
|
777
|
+
raise Phronomy::Error, "Agent #{agent_id.inspect} is being purged"
|
|
778
|
+
when :recovery_required
|
|
779
|
+
raise Phronomy::Error,
|
|
780
|
+
"Agent #{agent_id.inspect} ownership requires durable recovery/reconciliation"
|
|
781
|
+
else
|
|
782
|
+
raise Phronomy::RuntimeShutdownError,
|
|
783
|
+
"Agent #{agent_id.inspect} is detached from its Runtime"
|
|
784
|
+
end
|
|
583
785
|
end
|
|
584
786
|
|
|
585
|
-
|
|
787
|
+
def __assert_live_agent!
|
|
788
|
+
__assert_agent_accessible!
|
|
789
|
+
return true if @_phronomy_runtime_owner_state == :constructing
|
|
790
|
+
|
|
791
|
+
runtime = @_phronomy_runtime_owner
|
|
792
|
+
unless runtime&.__agent_owned?(self)
|
|
793
|
+
raise Phronomy::RuntimeShutdownError,
|
|
794
|
+
"Agent #{agent_id.inspect} is no longer the live owner in its Runtime"
|
|
795
|
+
end
|
|
796
|
+
true
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def _prepare_invocation_config(config, invocation_context)
|
|
800
|
+
__assert_live_agent!
|
|
801
|
+
_reject_removed_generic_identity_keys!(config)
|
|
802
|
+
effective_config = invocation_context ?
|
|
803
|
+
config.merge(invocation_context: invocation_context) : config
|
|
586
804
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
effective_config = config.merge(invocation_context: ic)
|
|
590
|
-
if effective_config[:cancellation_token].nil?
|
|
591
|
-
if (tok = ic.effective_timeout_token)
|
|
805
|
+
if invocation_context && effective_config[:cancellation_token].nil?
|
|
806
|
+
if (tok = invocation_context.effective_timeout_token)
|
|
592
807
|
effective_config = effective_config.merge(
|
|
593
808
|
cancellation_token: tok,
|
|
594
|
-
phronomy_timeout_deadline:
|
|
809
|
+
phronomy_timeout_deadline: invocation_context.deadline
|
|
595
810
|
)
|
|
596
811
|
end
|
|
597
812
|
end
|
|
598
|
-
|
|
813
|
+
effective_config
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def _reject_removed_generic_identity_keys!(config)
|
|
817
|
+
removed_key = [
|
|
818
|
+
:thread_id, "thread_id",
|
|
819
|
+
:session_id, "session_id",
|
|
820
|
+
:agent_invocation_id, "agent_invocation_id"
|
|
821
|
+
].find { |key| config.key?(key) }
|
|
822
|
+
return unless removed_key
|
|
823
|
+
|
|
824
|
+
raise ArgumentError,
|
|
825
|
+
"Agent identity #{removed_key.inspect} was removed; " \
|
|
826
|
+
"use purpose-specific domain identifiers or application tracing metadata"
|
|
599
827
|
end
|
|
600
828
|
|
|
601
829
|
def _check_event_loop_reentrancy(sync_method, async_method)
|
|
@@ -644,12 +872,12 @@ module Phronomy
|
|
|
644
872
|
end
|
|
645
873
|
end
|
|
646
874
|
|
|
647
|
-
def _report_stream_callback_error(callback_error, event:,
|
|
875
|
+
def _report_stream_callback_error(callback_error, event:, execution_id:,
|
|
648
876
|
callback_error_policy:)
|
|
649
877
|
lines = [
|
|
650
878
|
"[Phronomy] Stream callback failed",
|
|
651
879
|
"event=#{event.type.inspect}",
|
|
652
|
-
"
|
|
880
|
+
"execution_id=#{execution_id || "unknown"}",
|
|
653
881
|
"policy=#{callback_error_policy.inspect}",
|
|
654
882
|
"error=#{callback_error.class}: #{callback_error.message}"
|
|
655
883
|
]
|
|
@@ -704,7 +932,6 @@ module Phronomy
|
|
|
704
932
|
def _build_caller_meta(config)
|
|
705
933
|
meta = {}
|
|
706
934
|
meta[:user_id] = config[:user_id] if config[:user_id]
|
|
707
|
-
meta[:session_id] = config[:session_id] if config[:session_id]
|
|
708
935
|
if (ic = config[:invocation_context])
|
|
709
936
|
meta[:task_id] = ic.task_id if ic.task_id
|
|
710
937
|
meta[:parent_task_id] = ic.parent_task_id if ic.parent_task_id
|
|
@@ -825,17 +1052,23 @@ module Phronomy
|
|
|
825
1052
|
}
|
|
826
1053
|
end
|
|
827
1054
|
|
|
828
|
-
# Base#call_async ultimately executes #call, so the synchronous wrapper
|
|
829
|
-
# above already applies filters for ordinary Tools. Agent-backed Tools
|
|
830
|
-
# override #call_async and bypass #call; only those custom async Tools
|
|
831
|
-
# need an asynchronous result-filter wrapper here.
|
|
832
1055
|
if custom_async_call
|
|
833
1056
|
define_method(:call_async) do |args, **kwargs|
|
|
834
1057
|
source = super(args, **kwargs)
|
|
835
|
-
filtered = Phronomy::
|
|
1058
|
+
filtered = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
|
|
1059
|
+
name: "tool-filter-#{name}"
|
|
1060
|
+
)
|
|
1061
|
+
source_has_physical_signal = source.respond_to?(:on_physical_complete)
|
|
1062
|
+
source.on_physical_complete { filtered.mark_physical_complete! } if
|
|
1063
|
+
source_has_physical_signal
|
|
836
1064
|
source.on_complete do |value, error|
|
|
837
1065
|
if error
|
|
838
|
-
filtered.
|
|
1066
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
1067
|
+
if source.respond_to?(:status) && source.status == :cancelled
|
|
1068
|
+
filtered.cancel!(error)
|
|
1069
|
+
else
|
|
1070
|
+
filtered.fail(error)
|
|
1071
|
+
end
|
|
839
1072
|
next
|
|
840
1073
|
end
|
|
841
1074
|
|
|
@@ -843,8 +1076,10 @@ module Phronomy
|
|
|
843
1076
|
result = result_filters.inject(value) { |val, filter|
|
|
844
1077
|
filter.call(val, tool_name: name, args: args)
|
|
845
1078
|
}
|
|
1079
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
846
1080
|
filtered.complete(result)
|
|
847
1081
|
rescue => filter_error
|
|
1082
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
848
1083
|
filtered.fail(filter_error)
|
|
849
1084
|
end
|
|
850
1085
|
end
|