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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -5
  3. data/docs/application-recipes.md +229 -0
  4. data/docs/architecture/dx05-agent-scope.md +119 -0
  5. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  6. data/docs/architecture/error-api-review.md +245 -0
  7. data/docs/failure-reason-codes.md +45 -0
  8. data/docs/features.md +4 -1
  9. data/docs/getting-started.md +44 -43
  10. data/docs/ordered-event-delivery.md +54 -0
  11. data/docs/workflow-completion.md +92 -0
  12. data/gemfiles/http_minimum.gemfile +5 -0
  13. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  14. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  15. data/lib/phronomy/agent/api/agent.rb +13 -0
  16. data/lib/phronomy/agent/async_event_api.rb +111 -87
  17. data/lib/phronomy/agent/base.rb +74 -43
  18. data/lib/phronomy/agent/before_llm_input.rb +7 -1
  19. data/lib/phronomy/agent/execution/exact_execution.rb +11 -9
  20. data/lib/phronomy/agent/execution/execution_coordinator.rb +16 -13
  21. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -1
  22. data/lib/phronomy/agent/execution/initial_preparation.rb +1 -4
  23. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +1 -4
  24. data/lib/phronomy/agent/filterable.rb +21 -15
  25. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  26. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  27. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  28. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
  29. data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
  30. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
  31. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  32. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +3 -3
  33. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +7 -7
  34. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
  35. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +5 -9
  36. data/lib/phronomy/agent/recovery/recovery_support.rb +2 -8
  37. data/lib/phronomy/agent/stream_callback_error.rb +2 -2
  38. data/lib/phronomy/common/error.rb +65 -1
  39. data/lib/phronomy/content_store/base.rb +7 -2
  40. data/lib/phronomy/context/assembly.rb +11 -18
  41. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  42. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +5 -1
  43. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  44. data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
  45. data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
  46. data/lib/phronomy/execution/task_result.rb +7 -2
  47. data/lib/phronomy/filter/filter_block_error.rb +2 -2
  48. data/lib/phronomy/generation/low_confidence_error.rb +2 -2
  49. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  50. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
  51. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  52. data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
  53. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  54. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  55. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  56. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
  57. data/lib/phronomy/tool/base.rb +25 -23
  58. data/lib/phronomy/tracing/base.rb +35 -6
  59. data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
  60. data/lib/phronomy/version.rb +1 -1
  61. data/lib/phronomy/workflow/execution/workflow.rb +30 -2
  62. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
  63. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
  64. data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
  65. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  66. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  67. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  68. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
  69. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
  70. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  71. data/lib/phronomy.rb +6 -0
  72. data/scripts/api_snapshot.rb +12 -0
  73. data/scripts/check_readme_ruby.rb +24 -29
  74. data/scripts/check_readme_runnable.rb +92 -81
  75. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  76. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  77. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  78. data/sig/phronomy/agent.rbs +18 -0
  79. data/sig/phronomy/embeddings/base.rbs +6 -0
  80. data/sig/phronomy/execution/execution.rbs +2 -2
  81. data/sig/phronomy/extensions.rbs +1 -0
  82. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  83. data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
  84. data/sig/phronomy/integrations.rbs +14 -0
  85. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  86. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  87. data/sig/phronomy/storage/contracts.rbs +2 -2
  88. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
  89. data/sig/phronomy/workflow.rbs +4 -0
  90. data/sig/phronomy.rbs +4 -0
  91. metadata +49 -2
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ eval_gemfile "mcp_1_0.gemfile"
4
+ gem "faraday", "= 2.1.0"
5
+ gem "webmock", require: false
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../persistence/errors"
4
+
5
+ module Phronomy
6
+ # Only the requested Agent root is absent, not one of its referenced records.
7
+ # @api public
8
+ class AgentNotFoundError < Phronomy::Persistence::NotFoundError
9
+ attr_reader :agent_id
10
+
11
+ def initialize(agent_id:)
12
+ @agent_id = agent_id.to_s.dup.freeze
13
+ super("Agent not found: #{@agent_id}", code: "agent.not_found")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # An old Agent reference cannot act on a later incarnation of the same ID.
5
+ # @api public
6
+ class AgentUnloadedError < Phronomy::Error
7
+ attr_reader :agent_id
8
+
9
+ def initialize(agent_id:)
10
+ @agent_id = agent_id.to_s.dup.freeze
11
+ super("Agent #{@agent_id.inspect} has been unloaded", code: "agent.unloaded")
12
+ end
13
+ end
14
+ end
@@ -3,6 +3,17 @@
3
3
  module Phronomy
4
4
  module Agent
5
5
  StreamEvent = Data.define(:type, :payload)
6
+
7
+ # Own Agents acquired by the block, then attempt every release. A returned
8
+ # TaskResult is awaited and unwrapped; a plain value is returned unchanged.
9
+ # @yield [scope] the Runtime-bound owner of newly materialized Agents
10
+ # @yieldparam scope [Phronomy::Agent::Scope]
11
+ # @return [Object] the block value or resolved TaskResult value
12
+ # @raise [ScopeCleanupError] successful work whose Agent release failed
13
+ # @api public
14
+ def self.open_scope(&block)
15
+ Scope.send(:open, &block)
16
+ end
6
17
  end
7
18
  end
8
19
 
@@ -15,3 +26,5 @@ end
15
26
  # Durable Agent Recovery and Agent-incarnation event binding.
16
27
  require_relative "../recovery/recovery_support"
17
28
  require_relative "../recovery/recovery_coordinator"
29
+
30
+ Phronomy::Agent::Base.prepend(Phronomy::Agent::LifetimeApi)
@@ -13,31 +13,39 @@ module Phronomy
13
13
  # - Recovery resolution uses resolve/resolve_async.
14
14
  module AsyncEventApi
15
15
  def start_reserved_async(reservation:, input:, config: {})
16
- unless reservation.is_a?(ReservedExecution) && reservation.agent_id == agent_id
17
- raise ArgumentError, "reservation must belong to this Agent"
16
+ __with_operation(async: true) do
17
+ unless reservation.is_a?(ReservedExecution) && reservation.agent_id == agent_id
18
+ raise ArgumentError, "reservation must belong to this Agent"
19
+ end
20
+ config = __invocation_config(_prepare_invocation_config(config, nil))
21
+ ExactExecution.start(agent: self, execution_id: reservation.execution_id, input: input,
22
+ config: config.merge(phronomy_reservation: reservation.correlation))
18
23
  end
19
- config = __invocation_config(_prepare_invocation_config(config, nil))
20
- ExactExecution.start(agent: self, execution_id: reservation.execution_id, input: input,
21
- config: config.merge(phronomy_reservation: reservation.correlation))
22
24
  end
23
25
 
24
26
  def resume_async(execution_id, config: {})
25
- config = __invocation_config(_prepare_invocation_config(config, nil))
26
- ExactExecution.start(agent: self, execution_id: execution_id, input: nil, config: config, resume_only: true)
27
+ __with_operation(async: true) do
28
+ config = __invocation_config(_prepare_invocation_config(config, nil))
29
+ ExactExecution.start(agent: self, execution_id: execution_id, input: nil, config: config, resume_only: true)
30
+ end
27
31
  end
28
32
 
29
33
  def resume(execution_id, config: {})
30
- _check_event_loop_reentrancy(:resume, :resume_async)
31
- resume_async(execution_id, config: config).wait_result
34
+ __with_operation do
35
+ _check_event_loop_reentrancy(:resume, :resume_async)
36
+ resume_async(execution_id, config: config).wait_result
37
+ end
32
38
  end
33
39
 
34
40
  def cancel_async(execution_id)
35
- __assert_live_agent!
36
- environment = __execution_environment
37
- environment.submit(on_full: :raise) do
38
- requested = persistence.request_cancellation(agent_id: agent_id, execution_id: execution_id)
39
- ExecutionCancellation.signal(execution_id, agent_id, environment: environment) if requested
40
- persistence.observe_execution(agent_id: agent_id, execution_id: execution_id)
41
+ __with_operation(async: true) do
42
+ __assert_live_agent!
43
+ environment = __execution_environment
44
+ environment.submit(on_full: :raise) do
45
+ requested = persistence.request_cancellation(agent_id: agent_id, execution_id: execution_id)
46
+ ExecutionCancellation.signal(execution_id, agent_id, environment: environment) if requested
47
+ persistence.observe_execution(agent_id: agent_id, execution_id: execution_id)
48
+ end
41
49
  end
42
50
  end
43
51
 
@@ -48,15 +56,17 @@ module Phronomy
48
56
  **removed_options,
49
57
  &removed_block
50
58
  )
51
- _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
52
- config = _prepare_invocation_config(config, invocation_context)
53
- _check_event_loop_reentrancy(:invoke, :invoke_async)
54
- _start_agent_operation(
55
- input,
56
- config: config,
57
- mode: :invoke,
58
- listener: _phronomy_event_listener
59
- ).wait_result
59
+ __with_operation do
60
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
61
+ config = _prepare_invocation_config(config, invocation_context)
62
+ _check_event_loop_reentrancy(:invoke, :invoke_async)
63
+ _start_agent_operation(
64
+ input,
65
+ config: config,
66
+ mode: :invoke,
67
+ listener: _phronomy_event_listener
68
+ ).wait_result
69
+ end
60
70
  end
61
71
 
62
72
  def invoke_async(
@@ -66,14 +76,16 @@ module Phronomy
66
76
  **removed_options,
67
77
  &removed_block
68
78
  )
69
- _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
70
- config = _prepare_invocation_config(config, invocation_context)
71
- _start_agent_operation(
72
- input,
73
- config: config,
74
- mode: :invoke,
75
- listener: _phronomy_event_listener
76
- )
79
+ __with_operation(async: true) do
80
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
81
+ config = _prepare_invocation_config(config, invocation_context)
82
+ _start_agent_operation(
83
+ input,
84
+ config: config,
85
+ mode: :invoke,
86
+ listener: _phronomy_event_listener
87
+ )
88
+ end
77
89
  end
78
90
 
79
91
  def stream(
@@ -83,16 +95,18 @@ module Phronomy
83
95
  **removed_options,
84
96
  &removed_block
85
97
  )
86
- _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
87
- listener = _required_stream_listener!(:stream)
88
- config = _prepare_invocation_config(config, invocation_context)
89
- _check_event_loop_reentrancy(:stream, :stream_async)
90
- _start_agent_operation(
91
- input,
92
- config: config,
93
- mode: :stream,
94
- listener: listener
95
- ).wait_result
98
+ __with_operation do
99
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
100
+ listener = _required_stream_listener!(:stream)
101
+ config = _prepare_invocation_config(config, invocation_context)
102
+ _check_event_loop_reentrancy(:stream, :stream_async)
103
+ _start_agent_operation(
104
+ input,
105
+ config: config,
106
+ mode: :stream,
107
+ listener: listener
108
+ ).wait_result
109
+ end
96
110
  end
97
111
 
98
112
  def stream_async(
@@ -102,41 +116,47 @@ module Phronomy
102
116
  **removed_options,
103
117
  &removed_block
104
118
  )
105
- _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
106
- listener = _required_stream_listener!(:stream_async)
107
- config = _prepare_invocation_config(config, invocation_context)
108
- _start_agent_operation(
109
- input,
110
- config: config,
111
- mode: :stream,
112
- listener: listener
113
- )
119
+ __with_operation(async: true) do
120
+ _reject_removed_invocation_listener_arguments!(removed_options, removed_block)
121
+ listener = _required_stream_listener!(:stream_async)
122
+ config = _prepare_invocation_config(config, invocation_context)
123
+ _start_agent_operation(
124
+ input,
125
+ config: config,
126
+ mode: :stream,
127
+ listener: listener
128
+ )
129
+ end
114
130
  end
115
131
 
116
132
  def approve(execution_id, approval_request_id:, approved: true, config: {})
117
- _check_event_loop_reentrancy(:approve, :approve_async)
118
- approve_async(
119
- execution_id,
120
- approval_request_id: approval_request_id,
121
- approved: approved,
122
- config: config
123
- ).wait_result
133
+ __with_operation do
134
+ _check_event_loop_reentrancy(:approve, :approve_async)
135
+ approve_async(
136
+ execution_id,
137
+ approval_request_id: approval_request_id,
138
+ approved: approved,
139
+ config: config
140
+ ).wait_result
141
+ end
124
142
  end
125
143
 
126
144
  def approve_async(execution_id, approval_request_id:, approved: true, config: {})
127
- _reject_removed_generic_identity_keys!(config)
128
- owner = Phronomy::Agent::ExecutionRegistry.existing_current&.agent_execution_owner(execution_id)
129
- coordinator = if owner&.agent&.equal?(self)
130
- owner.coordinator
131
- else
132
- execution_coordinator_for(config)
145
+ __with_operation(async: true) do
146
+ _reject_removed_generic_identity_keys!(config)
147
+ owner = __execution_environment.existing_registry&.agent_execution_owner(execution_id)
148
+ coordinator = if owner&.agent&.equal?(self)
149
+ owner.coordinator
150
+ else
151
+ execution_coordinator_for(config)
152
+ end
153
+ coordinator.resume(
154
+ execution_id,
155
+ approval_request_id: approval_request_id,
156
+ approved: approved,
157
+ config: config
158
+ )
133
159
  end
134
- coordinator.resume(
135
- execution_id,
136
- approval_request_id: approval_request_id,
137
- approved: approved,
138
- config: config
139
- )
140
160
  end
141
161
 
142
162
  def resolve(
@@ -147,15 +167,17 @@ module Phronomy
147
167
  result: Phronomy::Agent::RecoveryRules::MISSING,
148
168
  error: Phronomy::Agent::RecoveryRules::MISSING
149
169
  )
150
- _check_event_loop_reentrancy(:resolve, :resolve_async)
151
- resolve_async(
152
- execution_id,
153
- expected_execution_revision: expected_execution_revision,
154
- subject: subject,
155
- outcome: outcome,
156
- result: result,
157
- error: error
158
- ).wait_result
170
+ __with_operation do
171
+ _check_event_loop_reentrancy(:resolve, :resolve_async)
172
+ resolve_async(
173
+ execution_id,
174
+ expected_execution_revision: expected_execution_revision,
175
+ subject: subject,
176
+ outcome: outcome,
177
+ result: result,
178
+ error: error
179
+ ).wait_result
180
+ end
159
181
  end
160
182
 
161
183
  def resolve_async(
@@ -166,14 +188,16 @@ module Phronomy
166
188
  result: Phronomy::Agent::RecoveryRules::MISSING,
167
189
  error: Phronomy::Agent::RecoveryRules::MISSING
168
190
  )
169
- Phronomy::Agent::RecoveryCoordinator.new(self).resolve(
170
- execution_id,
171
- expected_execution_revision: expected_execution_revision,
172
- subject: subject,
173
- outcome: outcome,
174
- result: result,
175
- error: error
176
- )
191
+ __with_operation(async: true) do
192
+ Phronomy::Agent::RecoveryCoordinator.new(self).resolve(
193
+ execution_id,
194
+ expected_execution_revision: expected_execution_revision,
195
+ subject: subject,
196
+ outcome: outcome,
197
+ result: result,
198
+ error: error
199
+ )
200
+ end
177
201
  end
178
202
 
179
203
  private
@@ -292,10 +292,14 @@ module Phronomy
292
292
  # @api private
293
293
  def __invocation_config(config) = config
294
294
 
295
- def knowledge_snapshot = persistence.knowledge_snapshot(agent_id: agent_id)
295
+ def knowledge_snapshot
296
+ __with_operation { persistence.knowledge_snapshot(agent_id: agent_id) }
297
+ end
296
298
 
297
299
  # Listener inheritance is an explicit orchestration choice, not a private getter.
298
- def event_listener = _phronomy_event_listener
300
+ def event_listener
301
+ __with_operation(read_only: true) { _phronomy_event_listener }
302
+ end
299
303
 
300
304
  attr_reader :agent_id, :persistence
301
305
 
@@ -371,65 +375,81 @@ module Phronomy
371
375
  end
372
376
 
373
377
  def agent_root
374
- __assert_agent_accessible!
375
- @root
378
+ __with_operation(read_only: true) do
379
+ __assert_agent_accessible!
380
+ @root
381
+ end
376
382
  end
377
383
 
378
384
  def journal_projection
379
- __assert_agent_accessible!
380
- Agent::JournalProjection.new(
381
- agent_root: @root,
382
- records: _journal_records_snapshot
383
- )
385
+ __with_operation(read_only: true) do
386
+ __assert_agent_accessible!
387
+ Agent::JournalProjection.new(
388
+ agent_root: @root,
389
+ records: _journal_records_snapshot
390
+ )
391
+ end
384
392
  end
385
393
 
386
394
  def transcript
387
- journal_projection.transcript_records
395
+ __with_operation(read_only: true) do
396
+ journal_projection.transcript_records
397
+ end
388
398
  end
389
399
 
390
400
  def clear_transcript!
391
- mutate_context!(:transcript_cleared) do |root|
392
- root.with(
393
- agent_revision: root.agent_revision + 1,
394
- context_revision: root.context_revision + 1,
395
- transcript_generation: root.transcript_generation + 1
396
- )
401
+ __with_operation do
402
+ mutate_context!(:transcript_cleared) do |root|
403
+ root.with(
404
+ agent_revision: root.agent_revision + 1,
405
+ context_revision: root.context_revision + 1,
406
+ transcript_generation: root.transcript_generation + 1
407
+ )
408
+ end
397
409
  end
398
410
  end
399
411
 
400
412
  def clear_knowledge!
401
- mutate_context!(:knowledge_cleared) do |root|
402
- root.with(
403
- agent_revision: root.agent_revision + 1,
404
- context_revision: root.context_revision + 1
405
- )
413
+ __with_operation do
414
+ mutate_context!(:knowledge_cleared) do |root|
415
+ root.with(
416
+ agent_revision: root.agent_revision + 1,
417
+ context_revision: root.context_revision + 1
418
+ )
419
+ end
406
420
  end
407
421
  end
408
422
 
409
423
  def add_knowledge(content, metadata: {})
410
- __assert_live_agent!
411
- current = agent_root
412
- update = state_writer.add_knowledge(root: current, content: content, metadata: metadata)
413
- apply_state_update(update)
414
- self
424
+ __with_operation do
425
+ __assert_live_agent!
426
+ current = agent_root
427
+ update = state_writer.add_knowledge(root: current, content: content, metadata: metadata)
428
+ apply_state_update(update)
429
+ self
430
+ end
415
431
  end
416
432
 
417
433
  def reset_context!
418
- mutate_context!(:context_reset) do |root|
419
- root.with(
420
- agent_revision: root.agent_revision + 1,
421
- context_revision: root.context_revision + 1,
422
- transcript_generation: root.transcript_generation + 1
423
- )
434
+ __with_operation do
435
+ mutate_context!(:context_reset) do |root|
436
+ root.with(
437
+ agent_revision: root.agent_revision + 1,
438
+ context_revision: root.context_revision + 1,
439
+ transcript_generation: root.transcript_generation + 1
440
+ )
441
+ end
424
442
  end
425
443
  end
426
444
 
427
445
  def close!
428
- mutate_context!(:agent_closed, context_affecting: false) do |root|
429
- root.with(
430
- agent_revision: root.agent_revision + 1,
431
- lifecycle_status: :closed
432
- )
446
+ __with_operation do
447
+ mutate_context!(:agent_closed, context_affecting: false) do |root|
448
+ root.with(
449
+ agent_revision: root.agent_revision + 1,
450
+ lifecycle_status: :closed
451
+ )
452
+ end
433
453
  end
434
454
  end
435
455
 
@@ -544,7 +564,7 @@ module Phronomy
544
564
 
545
565
  # @api private
546
566
  def __release_runtime_owner!(runtime)
547
- return self if @_phronomy_runtime_owner_state == :purged
567
+ return self if %i[purged unloaded].include?(@_phronomy_runtime_owner_state)
548
568
  return self unless @_phronomy_runtime_owner.equal?(runtime)
549
569
 
550
570
  @_phronomy_runtime_owner_state = :released
@@ -579,7 +599,12 @@ module Phronomy
579
599
  if load_existing
580
600
  root = records = nil
581
601
  @persistence.transaction do |tx|
582
- root = tx.agents.load(@agent_id)
602
+ begin
603
+ root = tx.agents.load(@agent_id)
604
+ rescue Phronomy::Persistence::NotFoundError => error
605
+ raise if error.is_a?(Phronomy::AgentNotFoundError)
606
+ raise Phronomy::AgentNotFoundError.new(agent_id: @agent_id), cause: error
607
+ end
583
608
  records = tx.journals.read(
584
609
  @agent_id,
585
610
  limit: root.journal_position
@@ -656,11 +681,13 @@ module Phronomy
656
681
  public
657
682
 
658
683
  def tool_approval_policy(&block)
659
- __assert_live_agent!
660
- raise ArgumentError, "tool_approval_policy requires a block" unless block
684
+ __with_operation do
685
+ __assert_live_agent!
686
+ raise ArgumentError, "tool_approval_policy requires a block" unless block
661
687
 
662
- _approval_configuration_mutex.synchronize { @tool_approval_policy = block }
663
- self
688
+ _approval_configuration_mutex.synchronize { @tool_approval_policy = block }
689
+ self
690
+ end
664
691
  end
665
692
 
666
693
  private
@@ -671,6 +698,8 @@ module Phronomy
671
698
  true
672
699
  when :purged
673
700
  raise Phronomy::AgentPurgedError, "Agent #{agent_id.inspect} has been purged"
701
+ when :unloaded
702
+ raise Phronomy::AgentUnloadedError.new(agent_id: agent_id)
674
703
  when :purging
675
704
  raise Phronomy::Error, "Agent #{agent_id.inspect} is being purged"
676
705
  when :recovery_required
@@ -728,10 +757,12 @@ module Phronomy
728
757
  end
729
758
 
730
759
  def _complete_result_task(task, result)
760
+ task.finish_operation! if task.respond_to?(:finish_operation!)
731
761
  task.complete(result)
732
762
  end
733
763
 
734
764
  def _fail_result_task(task, error)
765
+ task.finish_operation! if task.respond_to?(:finish_operation!)
735
766
  task.fail(error)
736
767
  end
737
768
 
@@ -19,7 +19,13 @@ module Phronomy
19
19
  def _before_llm_input = @before_llm_input
20
20
  end
21
21
 
22
- attr_accessor :before_llm_input
22
+ def before_llm_input
23
+ __with_operation(read_only: true) { @before_llm_input }
24
+ end
25
+
26
+ def before_llm_input=(value)
27
+ __with_operation { @before_llm_input = value }
28
+ end
23
29
 
24
30
  private
25
31
 
@@ -18,7 +18,7 @@ module Phronomy
18
18
  @agent, @id, @input, @config = agent, execution_id.to_s.freeze, input, config.freeze
19
19
  @resume_only = resume_only
20
20
  @environment = @agent.__execution_environment
21
- @completion = Phronomy::TaskResult.deferred(name: "exact-execution:#{@id}")
21
+ @completion = OperationResult.new(agent: @agent, name: "exact-execution:#{@id}")
22
22
  end
23
23
 
24
24
  def start
@@ -43,13 +43,14 @@ module Phronomy
43
43
  [:absent, nil]
44
44
  end
45
45
  end
46
+ @completion.track(preparation)
46
47
  preparation.on_complete do |prepared, error|
47
48
  if error
48
- @completion.fail(error)
49
+ @completion.settle(error: error)
49
50
  else
50
51
  disposition, result = prepared
51
52
  case disposition
52
- when :terminal then @completion.complete(result)
53
+ when :terminal then @completion.settle(result)
53
54
  when :absent
54
55
  source = @agent.send(:_start_agent_operation, @input,
55
56
  config: @config.merge(phronomy_reserved_execution_id: @id, phronomy_exact_observers: [@completion].freeze),
@@ -58,15 +59,15 @@ module Phronomy
58
59
  when :active
59
60
  command = Wait.new(coordinator: self, agent: @agent, execution_id: @id)
60
61
  posted = @environment.registry.post(command, completion: @completion)
61
- @completion.fail(Phronomy::RuntimeShutdownError.new("Exact execution observer rejected")) unless posted
62
+ @completion.settle(error: Phronomy::RuntimeShutdownError.new("Exact execution observer rejected")) unless posted
62
63
  end
63
64
  end
64
65
  rescue => failure
65
- @completion.fail(failure)
66
+ @completion.settle(error: failure)
66
67
  end
67
68
  @completion
68
69
  rescue => failure
69
- @completion.fail(failure)
70
+ @completion.settle(error: failure)
70
71
  @completion
71
72
  end
72
73
 
@@ -101,7 +102,7 @@ module Phronomy
101
102
  waiter.on_complete { |_result, failure| reconcile(failure) }
102
103
  @environment.registry.register_agent_completion_waiter(@id, waiter)
103
104
  rescue => failure
104
- @completion.fail(failure)
105
+ @completion.settle(error: failure)
105
106
  end
106
107
 
107
108
  private
@@ -157,11 +158,12 @@ module Phronomy
157
158
  ))
158
159
  end
159
160
  end
161
+ @completion.track(task)
160
162
  task.on_complete do |result, error|
161
- error ? @completion.fail(error) : @completion.complete(result)
163
+ error ? @completion.settle(error: error) : @completion.settle(result)
162
164
  end
163
165
  rescue => error
164
- @completion.fail(error)
166
+ @completion.settle(error: error)
165
167
  end
166
168
 
167
169
  def materialize(execution)