phronomy 0.17.0 → 0.19.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -1134
  3. data/README.md +70 -1280
  4. data/benchmark/bench_regression.rb +25 -5
  5. data/docs/changelog/0.14-and-earlier.md +1137 -0
  6. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  7. data/docs/decisions/009-state-store-abstraction.md +1 -1
  8. data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
  9. data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
  10. data/docs/features.md +93 -0
  11. data/docs/getting-started.md +387 -0
  12. data/docs/migrations/0.15.md +35 -0
  13. data/docs/migrations/0.16.md +43 -0
  14. data/docs/migrations/0.19.md +148 -0
  15. data/docs/runtime-and-concurrency.md +349 -0
  16. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  17. data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
  18. data/lib/phronomy/agent/async_event_api.rb +3 -3
  19. data/lib/phronomy/agent/base.rb +116 -55
  20. data/lib/phronomy/agent/context/capability/base.rb +13 -3
  21. data/lib/phronomy/agent/context_assembler.rb +13 -3
  22. data/lib/phronomy/agent/execution_coordinator.rb +423 -252
  23. data/lib/phronomy/agent/journal_projection.rb +5 -1
  24. data/lib/phronomy/agent/shared_state.rb +2 -0
  25. data/lib/phronomy/agent/tool_executor.rb +29 -71
  26. data/lib/phronomy/agent/tool_invocation.rb +97 -47
  27. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  28. data/lib/phronomy/configuration.rb +7 -30
  29. data/lib/phronomy/diagnostics.rb +12 -41
  30. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  31. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  32. data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
  33. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  34. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  35. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  36. data/lib/phronomy/engine/event_loop.rb +170 -193
  37. data/lib/phronomy/engine/fsm_session.rb +6 -4
  38. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  39. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  40. data/lib/phronomy/engine/runtime.rb +52 -158
  41. data/lib/phronomy/engine/task.rb +136 -277
  42. data/lib/phronomy/llm_adapter/base.rb +14 -14
  43. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  44. data/lib/phronomy/llm_adapter.rb +2 -2
  45. data/lib/phronomy/metrics.rb +15 -30
  46. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  47. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  48. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  49. data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
  50. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
  51. data/lib/phronomy/persistence/in_memory.rb +113 -8
  52. data/lib/phronomy/persistence.rb +12 -3
  53. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  54. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  55. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  56. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  57. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  58. data/lib/phronomy/testing/eval/runner.rb +52 -0
  59. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  60. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  61. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  62. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  63. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  64. data/lib/phronomy/testing/eval.rb +9 -0
  65. data/lib/phronomy/testing/fake_clock.rb +6 -53
  66. data/lib/phronomy/testing.rb +2 -6
  67. data/lib/phronomy/tools/agent.rb +141 -6
  68. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  69. data/lib/phronomy/vector_store/base.rb +2 -2
  70. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  71. data/lib/phronomy/version.rb +1 -1
  72. data/lib/phronomy/workflow.rb +10 -9
  73. data/lib/phronomy/workflow_runner.rb +363 -99
  74. data/lib/phronomy.rb +10 -4
  75. data/scripts/api_snapshot.rb +4 -4
  76. metadata +25 -33
  77. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  78. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  79. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  80. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  81. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  82. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  83. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  84. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  85. data/lib/phronomy/engine/task/backend.rb +0 -80
  86. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  87. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  88. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  89. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  90. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  91. data/lib/phronomy/engine/task_group.rb +0 -193
  92. data/lib/phronomy/eval/comparison.rb +0 -47
  93. data/lib/phronomy/eval/dataset.rb +0 -45
  94. data/lib/phronomy/eval/eval_case.rb +0 -17
  95. data/lib/phronomy/eval/eval_result.rb +0 -29
  96. data/lib/phronomy/eval/metrics.rb +0 -66
  97. data/lib/phronomy/eval/runner.rb +0 -94
  98. data/lib/phronomy/eval/scorer/base.rb +0 -22
  99. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  100. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  101. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  102. data/lib/phronomy/eval/scorer.rb +0 -9
  103. data/lib/phronomy/eval.rb +0 -7
  104. data/lib/phronomy/state_store/base.rb +0 -48
  105. data/lib/phronomy/state_store/in_memory.rb +0 -62
  106. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  107. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -10,9 +10,13 @@ module Phronomy
10
10
  ].freeze
11
11
  KNOWLEDGE_RESET_KINDS = %i[knowledge_cleared context_reset].freeze
12
12
 
13
- def initialize(persistence:, agent_root:)
13
+ def initialize(agent_root:, persistence: nil, records: nil)
14
14
  @persistence = persistence
15
15
  @agent_root = agent_root
16
+ @records = records&.dup&.freeze
17
+ if @records.nil? && @persistence.nil?
18
+ raise ArgumentError, "JournalProjection requires persistence: or records:"
19
+ end
16
20
  end
17
21
 
18
22
  def transcript_records
@@ -144,6 +144,7 @@ module Phronomy
144
144
  tool_name "read_store"
145
145
  description "Read all current findings from the shared knowledge store. " \
146
146
  "Call this to see what other researchers have discovered."
147
+ execution_mode :cooperative
147
148
 
148
149
  define_method(:execute) { store.read_all.to_json }
149
150
  end
@@ -152,6 +153,7 @@ module Phronomy
152
153
  tool_name "write_finding"
153
154
  description "Record a new finding into the shared knowledge store so " \
154
155
  "that other researchers can build on your discovery."
156
+ execution_mode :cooperative
155
157
  param :content, type: :string, desc: "The finding to record"
156
158
 
157
159
  define_method(:execute) do |content:|
@@ -2,90 +2,48 @@
2
2
 
3
3
  module Phronomy
4
4
  module Agent
5
- # Centralises Tool execution routing based on execution_mode.
5
+ # Routes Tool work according to the Tool execution contract.
6
6
  #
7
- # Tool-specific timeout and retry belong to the Tool implementation or its
8
- # underlying client. This executor only chooses the Phronomy execution
9
- # resource and propagates cooperative cancellation.
10
- # @api private
7
+ # :cooperative Tool calls execute inline and must return quickly. call_async
8
+ # wraps their result in an already-settled Task and never consumes an
9
+ # OffloadPool worker.
10
+ #
11
+ # :offloaded Tool calls route synchronous work through OffloadPool. Phronomy
12
+ # does not distinguish whether the reason is blocking I/O, CPU-bound work, or
13
+ # another long synchronous operation.
11
14
  module ToolExecutor
12
- WARNED_MODES = Set.new
13
- WARNED_MODES_MUTEX = Mutex.new
14
- private_constant :WARNED_MODES, :WARNED_MODES_MUTEX
15
-
16
- # Agent-owned execution boundary. Only a ToolInvocation that has consumed
17
- # authorization may enter this method.
18
- def self.call_invocation_async(
19
- tool_invocation:,
20
- cancellation_token: nil,
21
- config: {},
22
- runtime: Phronomy::Runtime.instance
23
- )
24
- unless tool_invocation.dispatchable?
25
- raise Phronomy::ToolError,
26
- "ToolInvocation #{tool_invocation.id} is not authorized for dispatch"
27
- end
28
-
29
- call_async(
30
- tool: tool_invocation.tool,
31
- args: tool_invocation.arguments,
32
- cancellation_token: cancellation_token,
33
- config: config,
34
- runtime: runtime
35
- )
36
- end
37
-
38
- # Low-level Tool API used by direct Tool#call_async callers. Agent execution
39
- # must use .call_invocation_async so authorization cannot be bypassed.
40
- #
41
- # +config+ remains available for invocation metadata, but Phronomy does not
42
- # interpret it as a Tool timeout or retry policy.
43
15
  def self.call_async(
44
16
  tool:,
45
17
  args:,
46
18
  cancellation_token: nil,
47
19
  config: {},
48
- runtime: Phronomy::Runtime.instance
20
+ runtime: nil,
21
+ on_full: :raise
49
22
  )
50
- ct = cancellation_token
51
23
  mode = tool.class.execution_mode
52
24
 
53
- if mode == :cpu_bound || mode == :external_process
54
- warn_key = [tool.class.name, mode]
55
- newly_warned = WARNED_MODES_MUTEX.synchronize { WARNED_MODES.add?(warn_key) }
56
- if newly_warned
57
- message = if mode == :cpu_bound
58
- "[Phronomy] Tool #{tool.class.name} declares execution_mode :cpu_bound, " \
59
- "which has no dedicated executor. Falling back to blocking_io " \
60
- "(BlockingAdapterPool). Use :blocking_io explicitly to suppress this warning."
61
- else
62
- "[Phronomy] Tool #{tool.class.name} declares execution_mode :external_process, " \
63
- "which has no dedicated process manager. Falling back to blocking_io " \
64
- "(BlockingAdapterPool)."
65
- end
66
- if Phronomy.configuration.logger
67
- Phronomy.configuration.logger.warn(message)
68
- else
69
- warn message
70
- end
25
+ case mode
26
+ when :cooperative
27
+ task = Phronomy::Task.deferred(name: "tool-#{tool.name}")
28
+ begin
29
+ task.complete(
30
+ tool.call(args, cancellation_token: cancellation_token)
31
+ )
32
+ rescue => error
33
+ task.fail(error)
71
34
  end
72
- mode = :blocking_io
73
- end
74
-
75
- pool = begin
76
- runtime&.blocking_io
77
- rescue
78
- nil
79
- end
80
-
81
- if mode == :cooperative || pool.nil?
82
- runtime.spawn(name: "tool-#{tool.class.name.to_s.split("::").last}") do
83
- tool.call(args, cancellation_token: ct)
35
+ task
36
+ when :offloaded
37
+ runtime ||= Phronomy::Runtime.instance
38
+ runtime.offload.submit(
39
+ cancellation_token: cancellation_token,
40
+ on_full: on_full
41
+ ) do
42
+ tool.call(args, cancellation_token: cancellation_token)
84
43
  end
85
44
  else
86
- pool.submit(cancellation_token: ct) do
87
- tool.call(args, cancellation_token: ct)
88
- end
45
+ raise Phronomy::ConfigurationError,
46
+ "unknown Tool execution_mode: #{mode.inspect}"
89
47
  end
90
48
  end
91
49
  end
@@ -119,8 +119,7 @@ module Phronomy
119
119
  end
120
120
 
121
121
  if schema_error
122
- if @tool.class.respond_to?(:on_schema_error) &&
123
- @tool.class.on_schema_error == :raise
122
+ if @tool.class.respond_to?(:on_schema_error) && @tool.class.on_schema_error == :raise
124
123
  @error = Phronomy::ToolError.new(
125
124
  "#{@tool.class.name} schema error: #{schema_error}"
126
125
  )
@@ -141,64 +140,75 @@ module Phronomy
141
140
  self
142
141
  end
143
142
 
144
- def authorization_task(runtime: Phronomy::Runtime.instance)
143
+ # Starts authorization and reports exactly one AuthorizationOutcome through
144
+ # the callback. No Task is created.
145
+ def start_authorization(runtime: Phronomy::Runtime.instance, &callback)
146
+ raise ArgumentError, "start_authorization requires a callback" unless callback
147
+
145
148
  pool = runtime.pool(
146
149
  :authorization,
147
150
  size: Phronomy.configuration.authorization_pool_size,
148
151
  queue_size: Phronomy.configuration.authorization_queue_size
149
152
  )
150
- timeout = @config.fetch(:authorization_timeout, Phronomy.configuration.authorization_timeout)
151
- pending = pool.submit(
153
+ timeout = @config.fetch(
154
+ :authorization_timeout,
155
+ Phronomy.configuration.authorization_timeout
156
+ )
157
+ operation = pool.submit(
152
158
  timeout: timeout,
153
159
  cancellation_token: @config[:cancellation_token],
154
160
  on_full: :raise
155
161
  ) { evaluate_authorization }
156
-
157
- task = Phronomy::Task.deferred(name: "tool-authorization:#{@tool_name}")
158
- pending.on_complete do |outcome, error|
159
- resolved = error ? authorization_failure_outcome(error) : outcome
160
- task.backend.unblock(resolved, nil)
161
- task.transition!(:completed, value: resolved)
162
+ operation.on_complete do |outcome, error|
163
+ callback.call(error ? authorization_failure_outcome(error) : outcome)
162
164
  end
163
- task
165
+ self
164
166
  rescue => error
165
- task = Phronomy::Task.deferred(name: "tool-authorization:#{@tool_name}")
166
- outcome = authorization_failure_outcome(error)
167
- task.backend.unblock(outcome, nil)
168
- task.transition!(:completed, value: outcome)
169
- task
167
+ callback.call(authorization_failure_outcome(error))
168
+ self
170
169
  end
171
170
 
172
- def execution_task(runtime: Phronomy::Runtime.instance)
173
- pending = Phronomy::Agent::ToolExecutor.call_invocation_async(
174
- tool_invocation: self,
175
- cancellation_token: @config[:cancellation_token],
176
- config: @config,
177
- runtime: runtime
178
- )
179
- task = Phronomy::Task.deferred(name: "tool-execution:#{@tool_name}")
180
- pending.on_complete do |result, error|
181
- outcome = if error
182
- ExecutionOutcome.new(
183
- error: error,
184
- cancelled: error.is_a?(Phronomy::CancellationError)
185
- )
186
- else
187
- ExecutionOutcome.new(result: result)
171
+ # Starts Tool execution and reports completion through the callback.
172
+ #
173
+ # Both core execution paths use Tool#call_async:
174
+ #
175
+ # - :cooperative returns a Task without consuming an OffloadPool worker.
176
+ # Ordinary cooperative Tools settle that Task inline; Agent-backed Tools
177
+ # may start child EventLoop/FSM work and settle later.
178
+ # - :offloaded returns an OffloadPool PendingOperation for synchronous work
179
+ # that must not occupy the EventLoop.
180
+ #
181
+ # In either case ToolInvocation remains in :running and resumes only from
182
+ # the explicit :execution_completed FSM event posted by the session builder.
183
+ def start_execution(runtime: Phronomy::Runtime.instance, &callback)
184
+ raise ArgumentError, "start_execution requires a callback" unless callback
185
+ unless dispatchable?
186
+ callback.call(ExecutionOutcome.new(error: Phronomy::ToolError.new(
187
+ "ToolInvocation #{@id} is not authorized for dispatch"
188
+ )))
189
+ return self
190
+ end
191
+
192
+ case @tool.class.execution_mode
193
+ when :cooperative, :offloaded
194
+ operation = start_async_tool_operation(runtime)
195
+ unless operation.respond_to?(:on_complete)
196
+ raise Phronomy::ToolError,
197
+ "Tool #{@tool.class.name}#call_async must return a completion handle"
188
198
  end
189
- task.backend.unblock(outcome, nil)
190
- task.transition!(:completed, value: outcome)
199
+
200
+ operation.on_complete do |result, error|
201
+ callback.call(execution_outcome(result, error))
202
+ end
203
+ else
204
+ callback.call(ExecutionOutcome.new(error: Phronomy::ConfigurationError.new(
205
+ "unknown Tool execution_mode: #{@tool.class.execution_mode.inspect}"
206
+ )))
191
207
  end
192
- task
208
+ self
193
209
  rescue => error
194
- task = Phronomy::Task.deferred(name: "tool-execution:#{@tool_name}")
195
- outcome = ExecutionOutcome.new(
196
- error: error,
197
- cancelled: error.is_a?(Phronomy::CancellationError)
198
- )
199
- task.backend.unblock(outcome, nil)
200
- task.transition!(:completed, value: outcome)
201
- task
210
+ callback.call(execution_outcome(nil, error))
211
+ self
202
212
  end
203
213
 
204
214
  def mark_awaiting_approval! = (@status = :awaiting_approval
@@ -256,6 +266,48 @@ module Phronomy
256
266
 
257
267
  private
258
268
 
269
+ # Runtime is framework execution infrastructure, not part of the public
270
+ # Tool#call_async protocol.
271
+ #
272
+ # Tools using Capability::Base's default async implementation are routed
273
+ # directly through ToolExecutor so this ToolInvocation can supply its
274
+ # owning Runtime internally. Tools that override #call_async (for example
275
+ # Agent-backed Tools) receive only the public Tool async keywords.
276
+ def start_async_tool_operation(runtime)
277
+ if uses_default_call_async?
278
+ Phronomy::Agent::ToolExecutor.call_async(
279
+ tool: @tool,
280
+ args: @arguments,
281
+ cancellation_token: @config[:cancellation_token],
282
+ config: @config,
283
+ runtime: runtime,
284
+ on_full: :raise
285
+ )
286
+ else
287
+ @tool.call_async(
288
+ @arguments,
289
+ cancellation_token: @config[:cancellation_token],
290
+ config: @config
291
+ )
292
+ end
293
+ end
294
+
295
+ def uses_default_call_async?
296
+ @tool.method(:call_async).owner ==
297
+ Phronomy::Agent::Context::Capability::Base
298
+ end
299
+
300
+ def execution_outcome(result, error)
301
+ if error
302
+ ExecutionOutcome.new(
303
+ error: error,
304
+ cancelled: error.is_a?(Phronomy::CancellationError)
305
+ )
306
+ else
307
+ ExecutionOutcome.new(result: result)
308
+ end
309
+ end
310
+
259
311
  def evaluate_authorization
260
312
  request = build_request(facts: {}, default_decision: nil)
261
313
  facts = evaluate_facts
@@ -272,9 +324,7 @@ module Phronomy
272
324
  end
273
325
 
274
326
  reason = if decision == :require_approval
275
- (@origin == :mcp) ?
276
- "MCP Tool execution requires approval" :
277
- "Tool execution requires approval"
327
+ (@origin == :mcp) ? "MCP Tool execution requires approval" : "Tool execution requires approval"
278
328
  end
279
329
  AuthorizationOutcome.new(decision: decision, facts: facts, reason: reason)
280
330
  end
@@ -5,18 +5,9 @@ require "state_machines"
5
5
  module Phronomy
6
6
  module Agent
7
7
  # Builds an FSMSession for one ToolInvocation.
8
- #
9
- # Authorization and execution Tasks are observed by ToolInvocation-specific
10
- # callbacks that post explicit FSM events. The generated phase machine does
11
- # not await Tasks.
12
- #
13
- # @api private
8
+ # Async completion is represented only by explicit FSM events.
14
9
  class ToolInvocationSessionBuilder
15
- AUTO_STATE_SET = {
16
- idle: true,
17
- validating: true,
18
- queued: true
19
- }.freeze
10
+ AUTO_STATE_SET = {idle: true, validating: true, queued: true}.freeze
20
11
 
21
12
  DECLARED_STATES = %i[
22
13
  idle validating authorizing awaiting_approval authorized queued running
@@ -27,42 +18,20 @@ module Phronomy
27
18
 
28
19
  EXTERNAL_EVENTS = {
29
20
  authorization_completed: [
30
- {from: :authorizing, to: :cancelled, guard: ->(ctx) {
31
- ctx.cancelled?
32
- }},
33
- {from: :authorizing, to: :failed, guard: ->(ctx) {
34
- ctx.failed?
35
- }},
36
- {from: :authorizing, to: :rejected, guard: ->(ctx) {
37
- ctx.rejected?
38
- }},
39
- {from: :authorizing, to: :awaiting_approval, guard: ->(ctx) {
40
- ctx.awaiting_approval?
41
- }},
42
- {from: :authorizing, to: :authorized, guard: ->(ctx) {
43
- ctx.authorized?
44
- }}
21
+ {from: :authorizing, to: :cancelled, guard: ->(ctx) { ctx.cancelled? }},
22
+ {from: :authorizing, to: :failed, guard: ->(ctx) { ctx.failed? }},
23
+ {from: :authorizing, to: :rejected, guard: ->(ctx) { ctx.rejected? }},
24
+ {from: :authorizing, to: :awaiting_approval, guard: ->(ctx) { ctx.awaiting_approval? }},
25
+ {from: :authorizing, to: :authorized, guard: ->(ctx) { ctx.authorized? }}
45
26
  ],
46
27
  execution_completed: [
47
- {from: :running, to: :cancelled, guard: ->(ctx) {
48
- ctx.cancelled?
49
- }},
50
- {from: :running, to: :failed, guard: ->(ctx) {
51
- ctx.failed?
52
- }},
53
- {from: :running, to: :completed, guard: ->(ctx) {
54
- ctx.execution_completed?
55
- }}
56
- ],
57
- approve: [
58
- {from: :awaiting_approval, to: :authorized, guard: nil}
59
- ],
60
- reject: [
61
- {from: :awaiting_approval, to: :rejected, guard: nil}
62
- ],
63
- dispatch: [
64
- {from: :authorized, to: :queued, guard: nil}
28
+ {from: :running, to: :cancelled, guard: ->(ctx) { ctx.cancelled? }},
29
+ {from: :running, to: :failed, guard: ->(ctx) { ctx.failed? }},
30
+ {from: :running, to: :completed, guard: ->(ctx) { ctx.execution_completed? }}
65
31
  ],
32
+ approve: [{from: :awaiting_approval, to: :authorized, guard: nil}],
33
+ reject: [{from: :awaiting_approval, to: :rejected, guard: nil}],
34
+ dispatch: [{from: :authorized, to: :queued, guard: nil}],
66
35
  cancel: [
67
36
  {from: :awaiting_approval, to: :cancelled, guard: nil},
68
37
  {from: :authorized, to: :cancelled, guard: nil},
@@ -71,14 +40,8 @@ module Phronomy
71
40
  ]
72
41
  }.freeze
73
42
 
74
- def self.build(
75
- tool_invocation:,
76
- runtime: Phronomy::Runtime.instance
77
- )
78
- build_session(
79
- tool_invocation: tool_invocation,
80
- runtime: runtime
81
- )
43
+ def self.build(tool_invocation:, runtime: Phronomy::Runtime.instance)
44
+ build_session(tool_invocation: tool_invocation, runtime: runtime)
82
45
  end
83
46
 
84
47
  def self.build_for_resume(
@@ -125,31 +88,15 @@ module Phronomy
125
88
  def self.build_entry_actions(runtime)
126
89
  {
127
90
  validating: [method(:validating_action)],
128
- authorizing: [
129
- method(:authorizing_action).curry.call(runtime)
130
- ],
131
- awaiting_approval: [
132
- method(:awaiting_approval_action).curry.call(runtime)
133
- ],
134
- authorized: [
135
- method(:authorized_action).curry.call(runtime)
136
- ],
91
+ authorizing: [method(:authorizing_action).curry.call(runtime)],
92
+ awaiting_approval: [method(:awaiting_approval_action).curry.call(runtime)],
93
+ authorized: [method(:authorized_action).curry.call(runtime)],
137
94
  queued: [method(:queued_action)],
138
- running: [
139
- method(:running_action).curry.call(runtime)
140
- ],
141
- completed: [
142
- method(:completed_action).curry.call(runtime)
143
- ],
144
- failed: [
145
- method(:failed_action).curry.call(runtime)
146
- ],
147
- rejected: [
148
- method(:rejected_action).curry.call(runtime)
149
- ],
150
- cancelled: [
151
- method(:cancelled_action).curry.call(runtime)
152
- ]
95
+ running: [method(:running_action).curry.call(runtime)],
96
+ completed: [method(:completed_action).curry.call(runtime)],
97
+ failed: [method(:failed_action).curry.call(runtime)],
98
+ rejected: [method(:rejected_action).curry.call(runtime)],
99
+ cancelled: [method(:cancelled_action).curry.call(runtime)]
153
100
  }
154
101
  end
155
102
  private_class_method :build_entry_actions
@@ -176,59 +123,29 @@ module Phronomy
176
123
 
177
124
  event :state_completed do
178
125
  transition idle: :validating
179
-
180
- transition validating: :failed,
181
- if: ->(machine) { machine.context&.failed? }
182
- transition validating: :completed,
183
- if: ->(machine) {
184
- machine.context&.validation_completed?
185
- }
186
- transition validating: :authorizing,
187
- if: ->(machine) {
188
- machine.context&.validation_passed?
189
- }
190
-
126
+ transition validating: :failed, if: ->(m) { m.context&.failed? }
127
+ transition validating: :completed, if: ->(m) { m.context&.validation_completed? }
128
+ transition validating: :authorizing, if: ->(m) { m.context&.validation_passed? }
191
129
  transition queued: :running
192
130
  end
193
131
 
194
132
  event :authorization_completed do
195
- transition authorizing: :cancelled,
196
- if: ->(machine) { machine.context&.cancelled? }
197
- transition authorizing: :failed,
198
- if: ->(machine) { machine.context&.failed? }
199
- transition authorizing: :rejected,
200
- if: ->(machine) { machine.context&.rejected? }
201
- transition authorizing: :awaiting_approval,
202
- if: ->(machine) {
203
- machine.context&.awaiting_approval?
204
- }
205
- transition authorizing: :authorized,
206
- if: ->(machine) { machine.context&.authorized? }
133
+ transition authorizing: :cancelled, if: ->(m) { m.context&.cancelled? }
134
+ transition authorizing: :failed, if: ->(m) { m.context&.failed? }
135
+ transition authorizing: :rejected, if: ->(m) { m.context&.rejected? }
136
+ transition authorizing: :awaiting_approval, if: ->(m) { m.context&.awaiting_approval? }
137
+ transition authorizing: :authorized, if: ->(m) { m.context&.authorized? }
207
138
  end
208
139
 
209
140
  event :execution_completed do
210
- transition running: :cancelled,
211
- if: ->(machine) { machine.context&.cancelled? }
212
- transition running: :failed,
213
- if: ->(machine) { machine.context&.failed? }
214
- transition running: :completed,
215
- if: ->(machine) {
216
- machine.context&.execution_completed?
217
- }
218
- end
219
-
220
- event :approve do
221
- transition awaiting_approval: :authorized
222
- end
223
-
224
- event :reject do
225
- transition awaiting_approval: :rejected
226
- end
227
-
228
- event :dispatch do
229
- transition authorized: :queued
141
+ transition running: :cancelled, if: ->(m) { m.context&.cancelled? }
142
+ transition running: :failed, if: ->(m) { m.context&.failed? }
143
+ transition running: :completed, if: ->(m) { m.context&.execution_completed? }
230
144
  end
231
145
 
146
+ event(:approve) { transition awaiting_approval: :authorized }
147
+ event(:reject) { transition awaiting_approval: :rejected }
148
+ event(:dispatch) { transition authorized: :queued }
232
149
  event :cancel do
233
150
  transition awaiting_approval: :cancelled
234
151
  transition authorized: :cancelled
@@ -267,13 +184,9 @@ module Phronomy
267
184
  private_class_method :validating_action
268
185
 
269
186
  def self.authorizing_action(runtime, invocation)
270
- task = invocation.authorization_task(runtime: runtime)
271
- observe_task(
272
- runtime,
273
- invocation,
274
- task,
275
- event_type: :authorization_completed
276
- )
187
+ invocation.start_authorization(runtime: runtime) do |outcome|
188
+ post_to_invocation(runtime, invocation.id, :authorization_completed, outcome)
189
+ end
277
190
  invocation
278
191
  end
279
192
  private_class_method :authorizing_action
@@ -298,45 +211,14 @@ module Phronomy
298
211
  private_class_method :queued_action
299
212
 
300
213
  def self.running_action(runtime, invocation)
301
- # execution_task checks dispatchable? which requires :queued status;
302
- # mark_running! is deferred until after the task is started.
303
- task = invocation.execution_task(runtime: runtime)
214
+ invocation.start_execution(runtime: runtime) do |outcome|
215
+ post_to_invocation(runtime, invocation.id, :execution_completed, outcome)
216
+ end
304
217
  invocation.mark_running!
305
- observe_task(
306
- runtime,
307
- invocation,
308
- task,
309
- event_type: :execution_completed
310
- )
311
218
  invocation
312
219
  end
313
220
  private_class_method :running_action
314
221
 
315
- def self.observe_task(
316
- runtime,
317
- invocation,
318
- task,
319
- event_type:
320
- )
321
- task.on_complete do |outcome, error|
322
- payload = error || outcome
323
- accepted = runtime.event_loop.post_to_session(
324
- Phronomy::Event.new(
325
- type: event_type,
326
- target_id: invocation.id,
327
- payload: payload
328
- )
329
- )
330
- next if accepted
331
-
332
- Phronomy.configuration.logger&.warn(
333
- "[Phronomy] Dropped #{event_type.inspect} for " \
334
- "ToolInvocation #{invocation.id}"
335
- )
336
- end
337
- end
338
- private_class_method :observe_task
339
-
340
222
  def self.completed_action(runtime, invocation)
341
223
  notify_parent(runtime, invocation, :tool_completed)
342
224
  invocation
@@ -363,6 +245,18 @@ module Phronomy
363
245
  end
364
246
  private_class_method :cancelled_action
365
247
 
248
+ def self.post_to_invocation(runtime, id, event_type, payload)
249
+ accepted = runtime.event_loop.post_to_session(
250
+ Phronomy::Event.new(type: event_type, target_id: id, payload: payload)
251
+ )
252
+ return if accepted
253
+
254
+ Phronomy.configuration.logger&.warn(
255
+ "[Phronomy] Dropped #{event_type.inspect} for ToolInvocation #{id}"
256
+ )
257
+ end
258
+ private_class_method :post_to_invocation
259
+
366
260
  def self.notify_parent(runtime, invocation, event_type)
367
261
  runtime.event_loop.post_to_session(
368
262
  Phronomy::Event.new(