phronomy 0.22.0 → 0.23.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 +200 -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 +152 -0
- data/docs/architecture/persistence.md +175 -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 +82 -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 +112 -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/README.md +172 -0
- data/docs/features.md +31 -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/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- 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 +145 -72
- data/lib/phronomy/agent/base.rb +388 -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/execution_coordinator.rb +1975 -587
- 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 +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -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 +336 -102
- 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.rb +77 -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/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -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/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- 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 +1 -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/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/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- 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/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,216 @@ 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
|
|
150
|
+
|
|
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
|
|
228
160
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return defn if defn
|
|
234
|
-
klass = klass.superclass
|
|
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
|
|
272
268
|
end
|
|
273
269
|
|
|
274
270
|
attr_reader :agent_id, :persistence
|
|
275
271
|
|
|
276
272
|
def initialize(
|
|
277
|
-
agent_id:
|
|
273
|
+
agent_id: nil,
|
|
278
274
|
context: nil,
|
|
279
275
|
knowledge: [],
|
|
280
276
|
persistence: nil,
|
|
281
277
|
metadata: {},
|
|
282
|
-
load_existing: false
|
|
278
|
+
load_existing: false,
|
|
279
|
+
on_event: nil,
|
|
280
|
+
&event_block
|
|
283
281
|
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@
|
|
282
|
+
if on_event && event_block
|
|
283
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
284
|
+
end
|
|
285
|
+
@_phronomy_event_listener = on_event || event_block
|
|
288
286
|
|
|
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
|
|
287
|
+
reserved_agent_id = @_phronomy_reserved_agent_id
|
|
288
|
+
effective_agent_id = if agent_id.nil?
|
|
289
|
+
reserved_agent_id || SecureRandom.uuid.to_s
|
|
301
290
|
else
|
|
302
|
-
|
|
291
|
+
agent_id.to_s
|
|
292
|
+
end
|
|
293
|
+
raise ArgumentError, "agent_id must not be empty" if effective_agent_id.empty?
|
|
294
|
+
|
|
295
|
+
if @_phronomy_runtime_owner_state == :constructing
|
|
296
|
+
unless reserved_agent_id == effective_agent_id
|
|
297
|
+
raise Phronomy::Error,
|
|
298
|
+
"Agent initializer changed reserved identity from " \
|
|
299
|
+
"#{reserved_agent_id.inspect} to #{effective_agent_id.inspect}"
|
|
300
|
+
end
|
|
301
|
+
initialize_owned_state(
|
|
302
|
+
agent_id: effective_agent_id,
|
|
303
303
|
context: context,
|
|
304
304
|
knowledge: knowledge,
|
|
305
|
-
|
|
305
|
+
persistence: persistence,
|
|
306
|
+
metadata: metadata,
|
|
307
|
+
load_existing: load_existing
|
|
306
308
|
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
return
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
if load_existing
|
|
313
|
+
raise ArgumentError,
|
|
314
|
+
"load_existing: is an internal hydration option; use .load(agent_id, persistence:)"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
runtime = Phronomy::Runtime.instance
|
|
318
|
+
begin
|
|
319
|
+
runtime.__create_agent(effective_agent_id, expected_class: self.class) do |owner_runtime|
|
|
320
|
+
__prepare_runtime_owner!(owner_runtime, effective_agent_id)
|
|
321
|
+
initialize_owned_state(
|
|
322
|
+
agent_id: effective_agent_id,
|
|
323
|
+
context: context,
|
|
324
|
+
knowledge: knowledge,
|
|
325
|
+
persistence: persistence,
|
|
326
|
+
metadata: metadata,
|
|
327
|
+
load_existing: false
|
|
328
|
+
)
|
|
329
|
+
self
|
|
330
|
+
end
|
|
331
|
+
rescue Phronomy::Persistence::ConflictError => error
|
|
332
|
+
raise Phronomy::AgentAlreadyExistsError,
|
|
333
|
+
"Agent #{effective_agent_id.inspect} already exists durably: #{error.message}"
|
|
311
334
|
end
|
|
312
335
|
end
|
|
313
336
|
|
|
314
337
|
def agent_root
|
|
338
|
+
__assert_agent_accessible!
|
|
315
339
|
@root
|
|
316
340
|
end
|
|
317
341
|
|
|
318
342
|
def journal_projection
|
|
343
|
+
__assert_agent_accessible!
|
|
319
344
|
Agent::JournalProjection.new(
|
|
320
345
|
agent_root: @root,
|
|
321
346
|
records: _journal_records_snapshot
|
|
@@ -336,8 +361,6 @@ module Phronomy
|
|
|
336
361
|
end
|
|
337
362
|
end
|
|
338
363
|
|
|
339
|
-
# Logically clears all persistent Knowledge registered before this point.
|
|
340
|
-
# Raw Journal records remain append-only and are not deleted.
|
|
341
364
|
def clear_knowledge!
|
|
342
365
|
mutate_context!(:knowledge_cleared) do |root|
|
|
343
366
|
root.with(
|
|
@@ -347,9 +370,8 @@ module Phronomy
|
|
|
347
370
|
end
|
|
348
371
|
end
|
|
349
372
|
|
|
350
|
-
# Appends persistent Knowledge to the Agent Journal. The live Agent owns the
|
|
351
|
-
# current logical root/Journal view; Persistence is advanced optimistically.
|
|
352
373
|
def add_knowledge(content, metadata: {})
|
|
374
|
+
__assert_live_agent!
|
|
353
375
|
current = agent_root
|
|
354
376
|
next_root = nil
|
|
355
377
|
appended = nil
|
|
@@ -402,41 +424,188 @@ module Phronomy
|
|
|
402
424
|
end
|
|
403
425
|
|
|
404
426
|
def purge!
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
427
|
+
return true if @_phronomy_runtime_owner_state == :purged
|
|
428
|
+
|
|
429
|
+
__assert_live_agent!
|
|
430
|
+
runtime = @_phronomy_runtime_owner
|
|
431
|
+
token = runtime.__begin_agent_purge(self)
|
|
432
|
+
if runtime.__agent_execution_admitted?(agent_id)
|
|
433
|
+
runtime.__abort_agent_purge(self, token)
|
|
434
|
+
raise Phronomy::AgentBusyError,
|
|
435
|
+
"Agent #{agent_id.inspect} has a nonterminal top-level execution"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
begin
|
|
439
|
+
persistence.transaction do |tx|
|
|
440
|
+
tx.executions.assert_idle!(agent_id)
|
|
441
|
+
tx.journals.delete(agent_id)
|
|
442
|
+
tx.executions.delete_for_agent(agent_id)
|
|
443
|
+
tx.agents.delete(agent_id)
|
|
444
|
+
end
|
|
445
|
+
rescue Phronomy::AgentBusyError,
|
|
446
|
+
Phronomy::Persistence::ConflictError,
|
|
447
|
+
Phronomy::Persistence::NotFoundError,
|
|
448
|
+
Phronomy::Persistence::SerializationError,
|
|
449
|
+
ArgumentError
|
|
450
|
+
runtime.__abort_agent_purge(self, token)
|
|
451
|
+
raise
|
|
452
|
+
rescue
|
|
453
|
+
# Without F1 reconciliation support we cannot infer that a failed
|
|
454
|
+
# durable delete did not commit. Keep the identity fail-closed.
|
|
455
|
+
runtime.__leave_agent_purge_uncertain(self, token)
|
|
456
|
+
raise
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
runtime.__complete_agent_purge(self, token)
|
|
460
|
+
true
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Runtime ownership lifecycle hooks. These are internal coordination
|
|
464
|
+
# methods; application code must use new/create/load/get/purge!.
|
|
465
|
+
private
|
|
466
|
+
|
|
467
|
+
# @api private
|
|
468
|
+
def __prepare_runtime_owner!(runtime, reserved_agent_id)
|
|
469
|
+
@_phronomy_runtime_owner = runtime
|
|
470
|
+
@_phronomy_runtime_owner_state = :constructing
|
|
471
|
+
@_phronomy_reserved_agent_id = reserved_agent_id.to_s.freeze
|
|
472
|
+
self
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# @api private
|
|
476
|
+
def __bind_runtime_owner!(runtime)
|
|
477
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
478
|
+
@_phronomy_runtime_owner_state == :constructing
|
|
479
|
+
raise Phronomy::Error, "Agent Runtime ownership construction state is invalid"
|
|
480
|
+
end
|
|
481
|
+
@_phronomy_runtime_owner_state = :live
|
|
482
|
+
remove_instance_variable(:@_phronomy_reserved_agent_id) if
|
|
483
|
+
instance_variable_defined?(:@_phronomy_reserved_agent_id)
|
|
484
|
+
self
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# @api private
|
|
488
|
+
def __mark_purging!(runtime)
|
|
489
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
490
|
+
@_phronomy_runtime_owner_state == :live
|
|
491
|
+
raise Phronomy::Error, "Agent Runtime ownership purge state is invalid"
|
|
492
|
+
end
|
|
493
|
+
@_phronomy_runtime_owner_state = :purging
|
|
494
|
+
self
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# @api private
|
|
498
|
+
def __restore_live_after_purge_abort!(runtime)
|
|
499
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
500
|
+
@_phronomy_runtime_owner_state == :purging
|
|
501
|
+
raise Phronomy::Error, "Agent Runtime ownership purge rollback state is invalid"
|
|
502
|
+
end
|
|
503
|
+
@_phronomy_runtime_owner_state = :live
|
|
504
|
+
self
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# @api private
|
|
508
|
+
def __mark_ownership_recovery_required!(runtime)
|
|
509
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
510
|
+
%i[constructing purging].include?(@_phronomy_runtime_owner_state)
|
|
511
|
+
raise Phronomy::Error, "Agent Runtime ownership recovery state is invalid"
|
|
512
|
+
end
|
|
513
|
+
@_phronomy_runtime_owner_state = :recovery_required
|
|
514
|
+
self
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# @api private
|
|
518
|
+
def __mark_purged!(runtime)
|
|
519
|
+
unless @_phronomy_runtime_owner.equal?(runtime) &&
|
|
520
|
+
@_phronomy_runtime_owner_state == :purging
|
|
521
|
+
raise Phronomy::Error, "Agent Runtime ownership purge completion state is invalid"
|
|
410
522
|
end
|
|
411
523
|
@root = nil
|
|
412
524
|
@_phronomy_journal_records = [].freeze
|
|
413
|
-
|
|
525
|
+
@_phronomy_runtime_owner_state = :purged
|
|
526
|
+
self
|
|
414
527
|
end
|
|
415
528
|
|
|
416
|
-
#
|
|
529
|
+
# @api private
|
|
530
|
+
def __release_runtime_owner!(runtime)
|
|
531
|
+
return self if @_phronomy_runtime_owner_state == :purged
|
|
532
|
+
return self unless @_phronomy_runtime_owner.equal?(runtime)
|
|
533
|
+
|
|
534
|
+
@_phronomy_runtime_owner_state = :released
|
|
535
|
+
self
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
public
|
|
539
|
+
|
|
540
|
+
# Internal EventLoop apply hook used only after a successful durable
|
|
541
|
+
# operation result has been validated by ExecutionCoordinator.
|
|
417
542
|
def __replace_root(root)
|
|
418
543
|
@root = root
|
|
419
544
|
end
|
|
420
545
|
|
|
421
546
|
private
|
|
422
547
|
|
|
548
|
+
attr_reader :_phronomy_event_listener
|
|
549
|
+
|
|
550
|
+
def initialize_owned_state(
|
|
551
|
+
agent_id:,
|
|
552
|
+
context:,
|
|
553
|
+
knowledge:,
|
|
554
|
+
persistence:,
|
|
555
|
+
metadata:,
|
|
556
|
+
load_existing:
|
|
557
|
+
)
|
|
558
|
+
@persistence = persistence ||
|
|
559
|
+
Phronomy.configuration.persistence ||
|
|
560
|
+
Phronomy::Persistence::InMemory.new
|
|
561
|
+
@agent_id = agent_id.to_s.freeze
|
|
562
|
+
|
|
563
|
+
if load_existing
|
|
564
|
+
root = records = nil
|
|
565
|
+
@persistence.transaction do |tx|
|
|
566
|
+
root = tx.agents.load(@agent_id)
|
|
567
|
+
records = tx.journals.read(
|
|
568
|
+
@agent_id,
|
|
569
|
+
limit: root.journal_position
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
validate_loaded_definition!(root)
|
|
573
|
+
@root = root
|
|
574
|
+
@_phronomy_journal_records = Array(records).dup.freeze
|
|
575
|
+
else
|
|
576
|
+
@root = create_agent_root!(
|
|
577
|
+
context: context,
|
|
578
|
+
knowledge: knowledge,
|
|
579
|
+
metadata: metadata
|
|
580
|
+
)
|
|
581
|
+
@_phronomy_journal_records = @persistence.journals.read(
|
|
582
|
+
@agent_id,
|
|
583
|
+
limit: @root.journal_position
|
|
584
|
+
).dup.freeze
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
423
588
|
def validate_loaded_definition!(loaded)
|
|
424
589
|
definition = self.class.agent_definition
|
|
425
|
-
return if loaded
|
|
426
|
-
loaded.definition_version == definition.fetch(:version)
|
|
590
|
+
return if current_definition_compatible?(loaded, definition)
|
|
427
591
|
|
|
428
592
|
raise Phronomy::ConfigurationError,
|
|
429
593
|
"Agent definition mismatch for #{@agent_id}: stored " \
|
|
430
|
-
"#{loaded.agent_definition_id}@#{loaded.
|
|
594
|
+
"#{loaded.agent_definition_id}@#{loaded.agent_definition_version}, runtime " \
|
|
431
595
|
"#{definition.fetch(:id)}@#{definition.fetch(:version)}"
|
|
432
596
|
end
|
|
433
597
|
|
|
598
|
+
def current_definition_compatible?(loaded, runtime_definition)
|
|
599
|
+
loaded.agent_definition_id == runtime_definition.fetch(:id) &&
|
|
600
|
+
loaded.agent_definition_version == runtime_definition.fetch(:version)
|
|
601
|
+
end
|
|
602
|
+
|
|
434
603
|
def create_agent_root!(context:, knowledge:, metadata:)
|
|
435
604
|
definition = self.class.agent_definition
|
|
436
605
|
root = Agent::AgentRoot.create(
|
|
437
606
|
agent_id: agent_id,
|
|
438
607
|
agent_definition_id: definition.fetch(:id),
|
|
439
|
-
|
|
608
|
+
agent_definition_version: definition.fetch(:version),
|
|
440
609
|
metadata: metadata
|
|
441
610
|
)
|
|
442
611
|
persistence.transaction do |tx|
|
|
@@ -507,6 +676,7 @@ module Phronomy
|
|
|
507
676
|
end
|
|
508
677
|
|
|
509
678
|
def mutate_context!(kind, context_affecting: true)
|
|
679
|
+
__assert_live_agent!
|
|
510
680
|
current = agent_root
|
|
511
681
|
next_root = nil
|
|
512
682
|
appended = nil
|
|
@@ -541,7 +711,8 @@ module Phronomy
|
|
|
541
711
|
end
|
|
542
712
|
|
|
543
713
|
def yield_context_revision(current, proposed)
|
|
544
|
-
(proposed.context_revision == current.context_revision) ?
|
|
714
|
+
(proposed.context_revision == current.context_revision) ?
|
|
715
|
+
current.context_revision + 1 : proposed.context_revision
|
|
545
716
|
end
|
|
546
717
|
|
|
547
718
|
def _journal_records_snapshot
|
|
@@ -558,44 +729,73 @@ module Phronomy
|
|
|
558
729
|
|
|
559
730
|
public
|
|
560
731
|
|
|
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
732
|
def tool_approval_policy(&block)
|
|
733
|
+
__assert_live_agent!
|
|
572
734
|
raise ArgumentError, "tool_approval_policy requires a block" unless block
|
|
573
735
|
|
|
574
736
|
_approval_configuration_mutex.synchronize { @tool_approval_policy = block }
|
|
575
737
|
self
|
|
576
738
|
end
|
|
577
739
|
|
|
578
|
-
|
|
579
|
-
raise ArgumentError, "on_tool_approval_required requires a block" unless block
|
|
740
|
+
private
|
|
580
741
|
|
|
581
|
-
|
|
582
|
-
|
|
742
|
+
def __assert_agent_accessible!
|
|
743
|
+
case @_phronomy_runtime_owner_state
|
|
744
|
+
when :constructing, :live
|
|
745
|
+
true
|
|
746
|
+
when :purged
|
|
747
|
+
raise Phronomy::AgentPurgedError, "Agent #{agent_id.inspect} has been purged"
|
|
748
|
+
when :purging
|
|
749
|
+
raise Phronomy::Error, "Agent #{agent_id.inspect} is being purged"
|
|
750
|
+
when :recovery_required
|
|
751
|
+
raise Phronomy::Error,
|
|
752
|
+
"Agent #{agent_id.inspect} ownership requires durable recovery/reconciliation"
|
|
753
|
+
else
|
|
754
|
+
raise Phronomy::RuntimeShutdownError,
|
|
755
|
+
"Agent #{agent_id.inspect} is detached from its Runtime"
|
|
756
|
+
end
|
|
583
757
|
end
|
|
584
758
|
|
|
585
|
-
|
|
759
|
+
def __assert_live_agent!
|
|
760
|
+
__assert_agent_accessible!
|
|
761
|
+
return true if @_phronomy_runtime_owner_state == :constructing
|
|
586
762
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
763
|
+
runtime = @_phronomy_runtime_owner
|
|
764
|
+
unless runtime&.__agent_owned?(self)
|
|
765
|
+
raise Phronomy::RuntimeShutdownError,
|
|
766
|
+
"Agent #{agent_id.inspect} is no longer the live owner in its Runtime"
|
|
767
|
+
end
|
|
768
|
+
true
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def _prepare_invocation_config(config, invocation_context)
|
|
772
|
+
__assert_live_agent!
|
|
773
|
+
_reject_removed_generic_identity_keys!(config)
|
|
774
|
+
effective_config = invocation_context ?
|
|
775
|
+
config.merge(invocation_context: invocation_context) : config
|
|
776
|
+
|
|
777
|
+
if invocation_context && effective_config[:cancellation_token].nil?
|
|
778
|
+
if (tok = invocation_context.effective_timeout_token)
|
|
592
779
|
effective_config = effective_config.merge(
|
|
593
780
|
cancellation_token: tok,
|
|
594
|
-
phronomy_timeout_deadline:
|
|
781
|
+
phronomy_timeout_deadline: invocation_context.deadline
|
|
595
782
|
)
|
|
596
783
|
end
|
|
597
784
|
end
|
|
598
|
-
|
|
785
|
+
effective_config
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def _reject_removed_generic_identity_keys!(config)
|
|
789
|
+
removed_key = [
|
|
790
|
+
:thread_id, "thread_id",
|
|
791
|
+
:session_id, "session_id",
|
|
792
|
+
:agent_invocation_id, "agent_invocation_id"
|
|
793
|
+
].find { |key| config.key?(key) }
|
|
794
|
+
return unless removed_key
|
|
795
|
+
|
|
796
|
+
raise ArgumentError,
|
|
797
|
+
"Agent identity #{removed_key.inspect} was removed; " \
|
|
798
|
+
"use purpose-specific domain identifiers or application tracing metadata"
|
|
599
799
|
end
|
|
600
800
|
|
|
601
801
|
def _check_event_loop_reentrancy(sync_method, async_method)
|
|
@@ -644,12 +844,12 @@ module Phronomy
|
|
|
644
844
|
end
|
|
645
845
|
end
|
|
646
846
|
|
|
647
|
-
def _report_stream_callback_error(callback_error, event:,
|
|
847
|
+
def _report_stream_callback_error(callback_error, event:, execution_id:,
|
|
648
848
|
callback_error_policy:)
|
|
649
849
|
lines = [
|
|
650
850
|
"[Phronomy] Stream callback failed",
|
|
651
851
|
"event=#{event.type.inspect}",
|
|
652
|
-
"
|
|
852
|
+
"execution_id=#{execution_id || "unknown"}",
|
|
653
853
|
"policy=#{callback_error_policy.inspect}",
|
|
654
854
|
"error=#{callback_error.class}: #{callback_error.message}"
|
|
655
855
|
]
|
|
@@ -704,7 +904,6 @@ module Phronomy
|
|
|
704
904
|
def _build_caller_meta(config)
|
|
705
905
|
meta = {}
|
|
706
906
|
meta[:user_id] = config[:user_id] if config[:user_id]
|
|
707
|
-
meta[:session_id] = config[:session_id] if config[:session_id]
|
|
708
907
|
if (ic = config[:invocation_context])
|
|
709
908
|
meta[:task_id] = ic.task_id if ic.task_id
|
|
710
909
|
meta[:parent_task_id] = ic.parent_task_id if ic.parent_task_id
|
|
@@ -825,17 +1024,23 @@ module Phronomy
|
|
|
825
1024
|
}
|
|
826
1025
|
end
|
|
827
1026
|
|
|
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
1027
|
if custom_async_call
|
|
833
1028
|
define_method(:call_async) do |args, **kwargs|
|
|
834
1029
|
source = super(args, **kwargs)
|
|
835
|
-
filtered = Phronomy::
|
|
1030
|
+
filtered = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
|
|
1031
|
+
name: "tool-filter-#{name}"
|
|
1032
|
+
)
|
|
1033
|
+
source_has_physical_signal = source.respond_to?(:on_physical_complete)
|
|
1034
|
+
source.on_physical_complete { filtered.mark_physical_complete! } if
|
|
1035
|
+
source_has_physical_signal
|
|
836
1036
|
source.on_complete do |value, error|
|
|
837
1037
|
if error
|
|
838
|
-
filtered.
|
|
1038
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
1039
|
+
if source.respond_to?(:status) && source.status == :cancelled
|
|
1040
|
+
filtered.cancel!(error)
|
|
1041
|
+
else
|
|
1042
|
+
filtered.fail(error)
|
|
1043
|
+
end
|
|
839
1044
|
next
|
|
840
1045
|
end
|
|
841
1046
|
|
|
@@ -843,8 +1048,10 @@ module Phronomy
|
|
|
843
1048
|
result = result_filters.inject(value) { |val, filter|
|
|
844
1049
|
filter.call(val, tool_name: name, args: args)
|
|
845
1050
|
}
|
|
1051
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
846
1052
|
filtered.complete(result)
|
|
847
1053
|
rescue => filter_error
|
|
1054
|
+
filtered.mark_physical_complete! unless source_has_physical_signal
|
|
848
1055
|
filtered.fail(filter_error)
|
|
849
1056
|
end
|
|
850
1057
|
end
|