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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +122 -1134
- data/README.md +70 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +93 -0
- data/docs/getting-started.md +387 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +349 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +116 -55
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +423 -252
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +7 -30
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +170 -193
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +52 -158
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +363 -99
- data/lib/phronomy.rb +10 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +25 -33
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -193
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- 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(
|
|
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
|
-
#
|
|
5
|
+
# Routes Tool work according to the Tool execution contract.
|
|
6
6
|
#
|
|
7
|
-
# Tool
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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:
|
|
20
|
+
runtime: nil,
|
|
21
|
+
on_full: :raise
|
|
49
22
|
)
|
|
50
|
-
ct = cancellation_token
|
|
51
23
|
mode = tool.class.execution_mode
|
|
52
24
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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(
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
self
|
|
164
166
|
rescue => error
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
208
|
+
self
|
|
193
209
|
rescue => error
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
32
|
-
}},
|
|
33
|
-
{from: :authorizing, to: :
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
],
|
|
141
|
-
|
|
142
|
-
|
|
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: :
|
|
181
|
-
|
|
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
|
-
|
|
197
|
-
transition authorizing: :
|
|
198
|
-
|
|
199
|
-
transition authorizing: :
|
|
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
|
-
|
|
212
|
-
transition running: :
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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(
|