phronomy 0.28.0 → 0.29.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/README.md +20 -5
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/dx05-agent-scope.md +119 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +4 -1
- data/docs/getting-started.md +44 -43
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +5 -0
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +111 -87
- data/lib/phronomy/agent/base.rb +74 -43
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +11 -9
- data/lib/phronomy/agent/execution/execution_coordinator.rb +16 -13
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +1 -4
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +1 -4
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +3 -3
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +7 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +5 -9
- data/lib/phronomy/agent/recovery/recovery_support.rb +2 -8
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +11 -18
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +5 -1
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/low_confidence_error.rb +2 -2
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/base.rb +35 -6
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +18 -0
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +1 -0
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +4 -0
- metadata +49 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Owns the Agents materialized within one application operation. A scope is
|
|
6
|
+
# Runtime-bound, process-local, and must not be persisted as Workflow state.
|
|
7
|
+
# @api public
|
|
8
|
+
class Scope
|
|
9
|
+
# @api private
|
|
10
|
+
def self.open
|
|
11
|
+
raise ArgumentError, "open_scope requires a block" unless block_given?
|
|
12
|
+
if Phronomy::WaitPolicy.blocking_forbidden?
|
|
13
|
+
raise Phronomy::EventLoopReentrancyError, "Agent.open_scope cannot block EventLoop"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
scope = new(environment: ExecutionEnvironment.current)
|
|
17
|
+
result = nil
|
|
18
|
+
begin
|
|
19
|
+
result = yield(scope)
|
|
20
|
+
result = result.wait_result if result.is_a?(Phronomy::TaskResult)
|
|
21
|
+
rescue Exception => primary # standard:disable Lint/RescueException
|
|
22
|
+
raise
|
|
23
|
+
ensure
|
|
24
|
+
failures = scope.send(:__close)
|
|
25
|
+
if primary
|
|
26
|
+
failures.each do |failure|
|
|
27
|
+
error = failure.fetch(:error)
|
|
28
|
+
Phronomy::RuntimeSettings.current.logger&.warn(
|
|
29
|
+
"[Agent scope] cleanup failed agent_id=#{failure.fetch(:agent_id).inspect}: #{error.class}: #{error.message}"
|
|
30
|
+
)
|
|
31
|
+
rescue
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
elsif failures.any?
|
|
35
|
+
raise ScopeCleanupError.new(failures: failures, result: result)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
private_class_method :open
|
|
41
|
+
|
|
42
|
+
def initialize(environment:)
|
|
43
|
+
@environment = environment
|
|
44
|
+
@ownership = environment.ownership
|
|
45
|
+
@owner_token = Object.new.freeze
|
|
46
|
+
@mutex = Mutex.new
|
|
47
|
+
@condition = ConditionVariable.new
|
|
48
|
+
@state = :open
|
|
49
|
+
@receipts = {}
|
|
50
|
+
@agents = {}
|
|
51
|
+
end
|
|
52
|
+
private_class_method :new
|
|
53
|
+
|
|
54
|
+
# @return [Agent::Base]
|
|
55
|
+
# @api public
|
|
56
|
+
def create(agent_class, **options, &listener)
|
|
57
|
+
check_blocking!
|
|
58
|
+
materialize(:create, agent_class, options, listener, async: false)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [TaskResult<Agent::Base>]
|
|
62
|
+
# @api public
|
|
63
|
+
def create_async(agent_class, **options, &listener)
|
|
64
|
+
materialize(:create, agent_class, options, listener, async: true)
|
|
65
|
+
rescue => error
|
|
66
|
+
Phronomy::TaskResult.failed(error)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Repeated load in this scope returns the same live owner. Supplying a new
|
|
70
|
+
# listener or different Store for an already live instance is rejected.
|
|
71
|
+
# @return [Agent::Base]
|
|
72
|
+
# @api public
|
|
73
|
+
def load(agent_class, agent_id, persistence:, **options, &listener)
|
|
74
|
+
check_blocking!
|
|
75
|
+
materialize(:load, agent_class, options.merge(agent_id: agent_id, persistence: persistence), listener, async: false)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [TaskResult<Agent::Base>]
|
|
79
|
+
# @api public
|
|
80
|
+
def load_async(agent_class, agent_id, persistence:, **options, &listener)
|
|
81
|
+
materialize(:load, agent_class, options.merge(agent_id: agent_id, persistence: persistence), listener, async: true)
|
|
82
|
+
rescue => error
|
|
83
|
+
Phronomy::TaskResult.failed(error)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @api private
|
|
87
|
+
def __close
|
|
88
|
+
agents = @mutex.synchronize do
|
|
89
|
+
@state = :closing
|
|
90
|
+
@condition.wait(@mutex) until @receipts.empty?
|
|
91
|
+
@agents.values.dup
|
|
92
|
+
end
|
|
93
|
+
failures = []
|
|
94
|
+
agents.each do |agent|
|
|
95
|
+
agent.unload!
|
|
96
|
+
rescue => error
|
|
97
|
+
failures << {agent_id: agent.agent_id, error: error}.freeze
|
|
98
|
+
end
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
@agents.clear
|
|
101
|
+
@environment = @ownership = @owner_token = nil
|
|
102
|
+
@state = :closed
|
|
103
|
+
end
|
|
104
|
+
failures.freeze
|
|
105
|
+
end
|
|
106
|
+
private :__close
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def check_blocking!
|
|
111
|
+
return unless Phronomy::WaitPolicy.blocking_forbidden?
|
|
112
|
+
|
|
113
|
+
raise Phronomy::EventLoopReentrancyError, "Agent scope materialization cannot block EventLoop; use create_async/load_async"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def materialize(kind, agent_class, options, listener, async:)
|
|
117
|
+
unless agent_class.is_a?(Class) && agent_class <= Base
|
|
118
|
+
raise ArgumentError, "agent_class must be an Agent::Base subclass"
|
|
119
|
+
end
|
|
120
|
+
if options[:on_event] && listener
|
|
121
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
122
|
+
end
|
|
123
|
+
if kind == :load && !options[:persistence]
|
|
124
|
+
raise ArgumentError, "persistence is required"
|
|
125
|
+
end
|
|
126
|
+
key = (options[:agent_id] || (SecureRandom.uuid if kind == :create)).to_s.dup.freeze
|
|
127
|
+
raise ArgumentError, "agent_id must not be empty" if key.empty?
|
|
128
|
+
options = options.merge(agent_id: key)
|
|
129
|
+
receipt = Object.new.freeze
|
|
130
|
+
@mutex.synchronize do
|
|
131
|
+
raise Phronomy::Error, "Agent scope is #{@state}" unless @state == :open
|
|
132
|
+
@receipts[receipt] = key
|
|
133
|
+
end
|
|
134
|
+
operation = -> { acquire(kind, agent_class, key, options, listener) }
|
|
135
|
+
if async
|
|
136
|
+
begin
|
|
137
|
+
task = @environment.submit(on_full: :raise, &operation)
|
|
138
|
+
task.on_physical_complete { finish_receipt(receipt) }
|
|
139
|
+
task
|
|
140
|
+
rescue
|
|
141
|
+
finish_receipt(receipt)
|
|
142
|
+
raise
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
begin
|
|
146
|
+
operation.call
|
|
147
|
+
ensure
|
|
148
|
+
finish_receipt(receipt)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def acquire(kind, agent_class, key, options, listener)
|
|
154
|
+
materialized = false
|
|
155
|
+
agent = @ownership.public_send(kind, key, expected_class: agent_class, scope_owner: @owner_token) do |environment|
|
|
156
|
+
materialized = true
|
|
157
|
+
instance = agent_class.send(:__construct_owned_agent, environment, key,
|
|
158
|
+
**options, load_existing: kind == :load, &listener)
|
|
159
|
+
RecoveryCoordinator.new(instance).recover_on_load! if kind == :load
|
|
160
|
+
instance
|
|
161
|
+
end
|
|
162
|
+
# Register before any result or post-materialization check reaches the
|
|
163
|
+
# caller, even when scope closure began while the worker was running.
|
|
164
|
+
@mutex.synchronize { @agents[agent.object_id] = agent }
|
|
165
|
+
if kind == :load
|
|
166
|
+
unless agent.persistence.equal?(options[:persistence])
|
|
167
|
+
raise Phronomy::ConfigurationError, "Agent #{key.inspect} is already live with a different Persistence instance"
|
|
168
|
+
end
|
|
169
|
+
if !materialized && (options[:on_event] || listener)
|
|
170
|
+
raise Phronomy::ConfigurationError, "Agent #{key.inspect} is already live; load cannot re-bind on_event"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
agent
|
|
174
|
+
rescue Phronomy::Persistence::ConflictError => error
|
|
175
|
+
raise unless kind == :create
|
|
176
|
+
raise Phronomy::AgentAlreadyExistsError, "Agent #{key.inspect} already exists durably: #{error.message}"
|
|
177
|
+
rescue => error
|
|
178
|
+
# A fail-closed construction remains reserved in C04. Do not purge or
|
|
179
|
+
# release that reservation just because no instance reached this scope.
|
|
180
|
+
report_materialization_error(key, error)
|
|
181
|
+
raise
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def finish_receipt(receipt)
|
|
185
|
+
@mutex.synchronize do
|
|
186
|
+
@receipts.delete(receipt)
|
|
187
|
+
@condition.broadcast
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def report_materialization_error(key, error)
|
|
192
|
+
Phronomy::RuntimeSettings.current.logger&.warn("[Agent scope] materialization failed agent_id=#{key.inspect}: #{error.class}: #{error.message}")
|
|
193
|
+
rescue
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Successful application work with one or more failed Agent releases.
|
|
6
|
+
# Original exceptions and the successful result remain available to the caller.
|
|
7
|
+
# @api public
|
|
8
|
+
class ScopeCleanupError < Phronomy::Error
|
|
9
|
+
attr_reader :failures, :result
|
|
10
|
+
|
|
11
|
+
def initialize(failures:, result:)
|
|
12
|
+
@failures = failures.map { |failure| failure.dup.freeze }.freeze
|
|
13
|
+
@result = result
|
|
14
|
+
super("Agent scope cleanup failed for: #{@failures.map { |f| f.fetch(:agent_id) }.join(", ")}",
|
|
15
|
+
code: "agent.scope_cleanup_failed")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Agent-owned two-phase release. The worker only checks durable facts; the
|
|
6
|
+
# EventLoop completes the exact process-local reservation after physical I/O.
|
|
7
|
+
# @api private
|
|
8
|
+
class UnloadTransition
|
|
9
|
+
Command = Data.define(:coordinator, :phase)
|
|
10
|
+
private_constant :Command
|
|
11
|
+
|
|
12
|
+
attr_reader :result
|
|
13
|
+
|
|
14
|
+
def initialize(agent:, environment:, ownership:)
|
|
15
|
+
@agent, @environment, @ownership = agent, environment, ownership
|
|
16
|
+
@result = Phronomy::TaskResult.deferred(name: "agent-unload:#{agent.agent_id}")
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@finished = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
post(:check)
|
|
23
|
+
@result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def deliver_on_event_loop(command)
|
|
27
|
+
if command.phase == :check
|
|
28
|
+
if @environment.existing_registry&.agent_execution_admitted?(@agent.agent_id)
|
|
29
|
+
raise Phronomy::AgentBusyError, "Agent #{@agent.agent_id.inspect} has a nonterminal execution"
|
|
30
|
+
end
|
|
31
|
+
agent_id, persistence = @agent.agent_id, @agent.persistence
|
|
32
|
+
task = @environment.submit(on_full: :raise) do
|
|
33
|
+
persistence.transaction do |tx|
|
|
34
|
+
tx.executions.assert_idle!(agent_id)
|
|
35
|
+
tx.guard_agent!(agent_id)
|
|
36
|
+
unless tx.retentions.list(agent_id).empty?
|
|
37
|
+
raise Phronomy::AgentBusyError, "Agent #{agent_id.inspect} is retained by durable coordination"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
task.on_complete do |_value, error|
|
|
42
|
+
@read_error = error
|
|
43
|
+
task.on_physical_complete { post(:apply) }
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
finish(@read_error)
|
|
47
|
+
end
|
|
48
|
+
rescue => error
|
|
49
|
+
finish(error)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def post(phase)
|
|
55
|
+
accepted = @environment.registry.post(Command.new(coordinator: self, phase: phase), completion: @result)
|
|
56
|
+
finish(Phronomy::RuntimeShutdownError.new("EventLoop rejected Agent unload #{phase}")) unless accepted
|
|
57
|
+
rescue => error
|
|
58
|
+
finish(error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def finish(error)
|
|
62
|
+
first = @mutex.synchronize do
|
|
63
|
+
next false if @finished
|
|
64
|
+
@finished = true
|
|
65
|
+
end
|
|
66
|
+
return unless first
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
@ownership.finish_unload(@agent, self, success: error.nil?)
|
|
70
|
+
rescue => ownership_error
|
|
71
|
+
error ||= ownership_error
|
|
72
|
+
end
|
|
73
|
+
@agent = @environment = @ownership = nil
|
|
74
|
+
error ? @result.fail(error) : @result.complete(true)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -52,9 +52,9 @@ module Phronomy
|
|
|
52
52
|
environment.registry.mark_agent_execution_admission(agent.agent_id,
|
|
53
53
|
execution_id: execution.execution_id, state: :recovery_required)
|
|
54
54
|
deliver_resolution_required(execution, coordination_recovery_descriptor(execution))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
agent.send(:_complete_result_task, completion, {execution_id: execution.execution_id,
|
|
56
|
+
execution_revision: execution.execution_revision,
|
|
57
|
+
recovery: :resolution_required}.freeze)
|
|
58
58
|
return
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -38,7 +38,7 @@ module Phronomy
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def prepare_plan(execution)
|
|
41
|
-
root = agent.
|
|
41
|
+
root = agent.send(:__root_for_operation)
|
|
42
42
|
manifest_ref = execution.metadata["manifest_ref"]
|
|
43
43
|
base_ref = execution.metadata["base_manifest_ref"] ||
|
|
44
44
|
manifest_ref
|
|
@@ -209,7 +209,7 @@ module Phronomy
|
|
|
209
209
|
}
|
|
210
210
|
)
|
|
211
211
|
end
|
|
212
|
-
command.completion
|
|
212
|
+
agent.send(:_complete_result_task, command.completion, agent)
|
|
213
213
|
return
|
|
214
214
|
end
|
|
215
215
|
|
|
@@ -246,7 +246,7 @@ module Phronomy
|
|
|
246
246
|
result: payload
|
|
247
247
|
)
|
|
248
248
|
end
|
|
249
|
-
command.completion
|
|
249
|
+
agent.send(:_complete_result_task, command.completion, agent)
|
|
250
250
|
when Phronomy::Agent::RecoveryRules::RESUMABLE
|
|
251
251
|
continue_resumable_on_event_loop(
|
|
252
252
|
execution,
|
|
@@ -288,7 +288,7 @@ module Phronomy
|
|
|
288
288
|
end
|
|
289
289
|
end
|
|
290
290
|
end
|
|
291
|
-
command.completion
|
|
291
|
+
agent.send(:_fail_result_task, command.completion, caught)
|
|
292
292
|
# simplecov:enable
|
|
293
293
|
end
|
|
294
294
|
|
|
@@ -317,7 +317,7 @@ module Phronomy
|
|
|
317
317
|
if framework_batch?(execution)
|
|
318
318
|
internal = Phronomy::TaskResult.deferred(name: "framework-tool-recovery:#{execution.execution_id}")
|
|
319
319
|
continue_recovery_on_event_loop(execution, internal, material: material)
|
|
320
|
-
|
|
320
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
321
321
|
return
|
|
322
322
|
end
|
|
323
323
|
|
|
@@ -359,11 +359,11 @@ module Phronomy
|
|
|
359
359
|
result_task: internal_task,
|
|
360
360
|
error: nil
|
|
361
361
|
))
|
|
362
|
-
|
|
362
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
363
363
|
when :recovery_tools_completed, :recovery_provider_completed, :recovery_resolved_failed
|
|
364
364
|
internal_task = Phronomy::TaskResult.deferred(name: "agent-recovery-auto:#{execution.execution_id}")
|
|
365
365
|
continue_recovery_on_event_loop(execution, internal_task, material: material)
|
|
366
|
-
|
|
366
|
+
agent.send(:_complete_result_task, completion, agent)
|
|
367
367
|
else
|
|
368
368
|
raise Phronomy::ExecutionRehydrationRequiredError,
|
|
369
369
|
"no automatic continuation for #{execution.phase.inspect}"
|
|
@@ -14,24 +14,22 @@ module Phronomy
|
|
|
14
14
|
request.execution_id
|
|
15
15
|
)
|
|
16
16
|
unless state && state.agent.equal?(agent)
|
|
17
|
-
request.completion
|
|
17
|
+
agent.send(:_fail_result_task, request.completion,
|
|
18
18
|
Phronomy::ExecutionRehydrationRequiredError.new(
|
|
19
19
|
"no live recovered execution #{request.execution_id}"
|
|
20
|
-
)
|
|
21
|
-
)
|
|
20
|
+
))
|
|
22
21
|
return
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
current = state.execution
|
|
26
25
|
unless current.execution_revision ==
|
|
27
26
|
request.expected_execution_revision
|
|
28
|
-
request.completion
|
|
27
|
+
agent.send(:_fail_result_task, request.completion,
|
|
29
28
|
Phronomy::Persistence::StateConflictError.new(
|
|
30
29
|
"Recovery resolution revision conflict: expected " \
|
|
31
30
|
"#{request.expected_execution_revision}, actual " \
|
|
32
31
|
"#{current.execution_revision}"
|
|
33
|
-
)
|
|
34
|
-
)
|
|
32
|
+
))
|
|
35
33
|
return
|
|
36
34
|
end
|
|
37
35
|
|
|
@@ -42,28 +40,26 @@ module Phronomy
|
|
|
42
40
|
descriptor.fetch(:subject),
|
|
43
41
|
request.subject
|
|
44
42
|
)
|
|
45
|
-
request.completion
|
|
43
|
+
agent.send(:_fail_result_task, request.completion,
|
|
46
44
|
ArgumentError.new(
|
|
47
45
|
"Recovery subject is not current for execution #{request.execution_id}"
|
|
48
|
-
)
|
|
49
|
-
)
|
|
46
|
+
))
|
|
50
47
|
return
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
unless Array(
|
|
54
51
|
descriptor.fetch(:allowed_outcomes)
|
|
55
52
|
).map(&:to_sym).include?(request.outcome)
|
|
56
|
-
request.completion
|
|
53
|
+
agent.send(:_fail_result_task, request.completion,
|
|
57
54
|
ArgumentError.new(
|
|
58
55
|
"Recovery outcome #{request.outcome.inspect} is not allowed for the current subject"
|
|
59
|
-
)
|
|
60
|
-
)
|
|
56
|
+
))
|
|
61
57
|
return
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
operation = ResolutionOperation.new(
|
|
65
61
|
execution: current,
|
|
66
|
-
root: agent.
|
|
62
|
+
root: agent.send(:__root_for_operation),
|
|
67
63
|
subject: request.subject,
|
|
68
64
|
outcome: request.outcome,
|
|
69
65
|
result: request.result,
|
|
@@ -77,6 +73,7 @@ module Phronomy
|
|
|
77
73
|
task = @environment.submit(on_full: :raise) do
|
|
78
74
|
prepare_resolution(operation)
|
|
79
75
|
end
|
|
76
|
+
request.completion.track(task) if request.completion.respond_to?(:track)
|
|
80
77
|
task.on_complete do |result, error|
|
|
81
78
|
ready = ResolveReady.new(
|
|
82
79
|
coordinator: self,
|
|
@@ -86,15 +83,14 @@ module Phronomy
|
|
|
86
83
|
error: error
|
|
87
84
|
)
|
|
88
85
|
unless post_control(ready)
|
|
89
|
-
request.completion
|
|
86
|
+
agent.send(:_fail_result_task, request.completion,
|
|
90
87
|
Phronomy::RuntimeShutdownError.new(
|
|
91
88
|
"EventLoop rejected Recovery resolution apply"
|
|
92
|
-
)
|
|
93
|
-
)
|
|
89
|
+
))
|
|
94
90
|
end
|
|
95
91
|
end
|
|
96
92
|
rescue => caught
|
|
97
|
-
request.completion
|
|
93
|
+
agent.send(:_fail_result_task, request.completion, caught)
|
|
98
94
|
end
|
|
99
95
|
|
|
100
96
|
def perform_resolution(operation)
|
|
@@ -498,7 +494,7 @@ module Phronomy
|
|
|
498
494
|
|
|
499
495
|
continue_recovery_on_event_loop(result.execution, request.completion, material: result.material)
|
|
500
496
|
rescue => caught
|
|
501
|
-
request.completion
|
|
497
|
+
agent.send(:_fail_result_task, request.completion, caught)
|
|
502
498
|
end
|
|
503
499
|
end
|
|
504
500
|
end
|
|
@@ -147,9 +147,8 @@ module Phronomy
|
|
|
147
147
|
failure = error_present ?
|
|
148
148
|
RecoverySupport.resolution_failure(error) : nil
|
|
149
149
|
|
|
150
|
-
completion =
|
|
151
|
-
name: "agent-recovery-resolve:#{execution_id}"
|
|
152
|
-
)
|
|
150
|
+
completion = OperationResult.new(agent: agent,
|
|
151
|
+
name: "agent-recovery-resolve:#{execution_id}")
|
|
153
152
|
command = ResolveCommand.new(
|
|
154
153
|
coordinator: self,
|
|
155
154
|
execution_id: execution_id.to_s.freeze,
|
|
@@ -162,18 +161,15 @@ module Phronomy
|
|
|
162
161
|
completion: completion
|
|
163
162
|
)
|
|
164
163
|
unless post_control(command)
|
|
165
|
-
|
|
166
|
-
Phronomy::RuntimeShutdownError.new(
|
|
167
|
-
"EventLoop rejected Agent Recovery resolution"
|
|
168
|
-
)
|
|
169
|
-
)
|
|
164
|
+
agent.send(:_fail_result_task, completion,
|
|
165
|
+
Phronomy::RuntimeShutdownError.new("EventLoop rejected Agent Recovery resolution"))
|
|
170
166
|
end
|
|
171
167
|
completion
|
|
172
168
|
rescue => caught
|
|
173
169
|
completion ||= Phronomy::TaskResult.deferred(
|
|
174
170
|
name: "agent-recovery-resolve:#{execution_id}"
|
|
175
171
|
)
|
|
176
|
-
|
|
172
|
+
agent.send(:_fail_result_task, completion, caught)
|
|
177
173
|
completion
|
|
178
174
|
end
|
|
179
175
|
|
|
@@ -26,17 +26,11 @@ module Phronomy
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def resolution_failure(error)
|
|
29
|
-
|
|
30
|
-
"class" => error.class.name.to_s,
|
|
31
|
-
"message" => error.message.to_s
|
|
32
|
-
}.freeze
|
|
29
|
+
Phronomy::Error.diagnostic(error).freeze
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
def error_from_failure(failure)
|
|
36
|
-
|
|
37
|
-
Phronomy::Error.new(
|
|
38
|
-
"#{hash.fetch("class", "Error")}: #{hash.fetch("message", "Recovery-resolved failure")}"
|
|
39
|
-
)
|
|
33
|
+
Phronomy::Error.from_diagnostic(failure)
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
def event_payload(execution, descriptor)
|
|
@@ -22,14 +22,14 @@ module Phronomy
|
|
|
22
22
|
# @return [Exception] original Application callback exception
|
|
23
23
|
attr_reader :original_error
|
|
24
24
|
|
|
25
|
-
def initialize(event_type:, original_error:, result: nil)
|
|
25
|
+
def initialize(event_type:, original_error:, result: nil, code: nil)
|
|
26
26
|
@event_type = event_type
|
|
27
27
|
@original_error = original_error
|
|
28
28
|
@result = result
|
|
29
29
|
|
|
30
30
|
super(
|
|
31
31
|
"Stream callback failed while handling #{event_type.inspect}: " \
|
|
32
|
-
"#{original_error.class}: #{original_error.message}"
|
|
32
|
+
"#{original_error.class}: #{original_error.message}", code: code
|
|
33
33
|
)
|
|
34
34
|
set_backtrace(original_error.backtrace)
|
|
35
35
|
end
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
class Error < StandardError
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
# Optional stable reason supplied by the owner of the failed operation.
|
|
6
|
+
# It does not imply retry safety or replace execution status.
|
|
7
|
+
# @api public
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
# Existing message-only construction retains StandardError behavior.
|
|
11
|
+
# @api public
|
|
12
|
+
def initialize(message = nil, code: nil)
|
|
13
|
+
raise ArgumentError, "error code must be a String or nil" unless code.nil? || code.is_a?(String)
|
|
14
|
+
@code = code&.dup&.freeze
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Generic diagnostic transport. No domain-specific interpretation.
|
|
19
|
+
# External exceptions are not extended with this protocol.
|
|
20
|
+
# @api private
|
|
21
|
+
def self.diagnostic(error)
|
|
22
|
+
diagnostic = {"class" => error.class.name, "message" => error.message}
|
|
23
|
+
diagnostic["code"] = error.code if error.is_a?(Phronomy::Error) && error.code
|
|
24
|
+
cause = error.cause
|
|
25
|
+
diagnostic["cause"] = cause_diagnostic(cause, remaining: 3, seen: [error.object_id]) if cause
|
|
26
|
+
diagnostic
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Restores data, never a saved class name or arbitrary instance attributes.
|
|
30
|
+
# Restored causes are generic errors as well. Old diagnostics remain valid.
|
|
31
|
+
# @api private
|
|
32
|
+
def self.from_diagnostic(value, remaining: 3)
|
|
33
|
+
hash = value.to_h { |key, item| [key.to_s, item] }
|
|
34
|
+
error = Phronomy::Error.new(
|
|
35
|
+
"#{hash.fetch("class", "Error")}: #{hash.fetch("message", "Recovery-resolved failure")}",
|
|
36
|
+
code: hash["code"]
|
|
37
|
+
)
|
|
38
|
+
return error unless remaining.positive? && hash["cause"].is_a?(Hash) && hash["cause"].key?("class")
|
|
39
|
+
|
|
40
|
+
cause = from_diagnostic(hash["cause"], remaining: remaining - 1)
|
|
41
|
+
begin
|
|
42
|
+
raise error, cause: cause
|
|
43
|
+
rescue Phronomy::Error => restored
|
|
44
|
+
restored.set_backtrace(nil)
|
|
45
|
+
restored
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.cause_diagnostic(error, remaining:, seen:)
|
|
50
|
+
return {"truncated" => true} if remaining.zero? || seen.include?(error.object_id)
|
|
51
|
+
|
|
52
|
+
# Bound newly added cause data; retain the existing top-level format.
|
|
53
|
+
fields = {"class" => error.class.name, "message" => error.message}
|
|
54
|
+
fields["code"] = error.code if error.is_a?(Phronomy::Error) && error.code
|
|
55
|
+
diagnostic = fields.to_h do |key, value|
|
|
56
|
+
[key, value.is_a?(String) ? value[0, 2048] : value]
|
|
57
|
+
end
|
|
58
|
+
# Never turn a truncated reason into a different, apparently valid code.
|
|
59
|
+
diagnostic.delete("code") if fields["code"] && fields["code"].length > 2048
|
|
60
|
+
diagnostic["truncated"] = true if diagnostic != fields
|
|
61
|
+
if error.cause
|
|
62
|
+
diagnostic["cause"] = cause_diagnostic(error.cause,
|
|
63
|
+
remaining: remaining - 1, seen: seen + [error.object_id])
|
|
64
|
+
end
|
|
65
|
+
diagnostic
|
|
66
|
+
end
|
|
67
|
+
private_class_method :cause_diagnostic
|
|
68
|
+
end
|
|
5
69
|
end
|
|
@@ -28,13 +28,18 @@ module Phronomy
|
|
|
28
28
|
def fetch_text(content_id)
|
|
29
29
|
value = fetch(content_id).force_encoding(Encoding::UTF_8)
|
|
30
30
|
unless value.valid_encoding?
|
|
31
|
-
raise IntegrityError
|
|
31
|
+
raise IntegrityError.new("content is not UTF-8: #{content_id}", code: "content.invalid_encoding")
|
|
32
32
|
end
|
|
33
33
|
value
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def fetch_json(content_id)
|
|
37
|
-
|
|
37
|
+
bytes = fetch(content_id)
|
|
38
|
+
begin
|
|
39
|
+
Phronomy::CanonicalJSON.load(bytes)
|
|
40
|
+
rescue JSON::ParserError => error
|
|
41
|
+
raise IntegrityError.new("content is not JSON: #{content_id}", code: "content.invalid_json"), cause: error
|
|
42
|
+
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
def fetch_many(content_ids)
|
|
@@ -97,24 +97,17 @@ module Phronomy
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
99
|
def invoke_policy(policy, policy_input)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
result = policy.call(policy_input)
|
|
113
|
-
tracer.finish_span(span)
|
|
114
|
-
result
|
|
115
|
-
rescue => error
|
|
116
|
-
tracer&.finish_span(span, error: error) if defined?(span) && span
|
|
117
|
-
raise
|
|
100
|
+
Phronomy::Tracing::Base.trace_with(
|
|
101
|
+
@tracer, "context_policy", record_output: false,
|
|
102
|
+
attributes: {agent_id: policy_input.agent_id,
|
|
103
|
+
execution_id: policy_input.execution_id,
|
|
104
|
+
call_sequence: policy_input.call_sequence,
|
|
105
|
+
policy_class: policy.class.name || policy.class.to_s,
|
|
106
|
+
instruction_count: policy_input.instruction.length,
|
|
107
|
+
knowledge_count: policy_input.knowledge.length,
|
|
108
|
+
tool_count: policy_input.tools.length,
|
|
109
|
+
conversation_group_count: policy_input.conversation.length}
|
|
110
|
+
) { [policy.call(policy_input), nil] }
|
|
118
111
|
end
|
|
119
112
|
|
|
120
113
|
def store_manifest(
|