phronomy 0.25.0 → 0.26.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 +31 -0
- data/CONTRIBUTING.md +4 -4
- data/README.md +10 -7
- data/benchmark/bench_regression.rb +1 -1
- data/docs/architecture/agent-context.md +1 -1
- data/docs/architecture/persistence.md +2 -2
- data/docs/architecture/tracing.md +3 -3
- data/docs/async-composition.md +205 -0
- data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
- data/docs/features.md +12 -9
- data/docs/getting-started.md +6 -6
- data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
- data/docs/persistence-backends.md +1 -1
- data/docs/runtime-and-concurrency.md +89 -29
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
- data/lib/phronomy/agent/base.rb +4 -10
- data/lib/phronomy/agent/exact_execution.rb +3 -3
- data/lib/phronomy/agent/execution_coordinator.rb +20 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
- data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
- data/lib/phronomy/agent/tool_executor.rb +4 -4
- data/lib/phronomy/agent/tool_invocation.rb +1 -1
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
- data/lib/phronomy/blocking.rb +15 -8
- data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
- data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
- data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
- data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
- data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
- data/lib/phronomy/engine/event_loop.rb +7 -7
- data/lib/phronomy/engine/fsm_session.rb +3 -3
- data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
- data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
- data/lib/phronomy/execution.rb +236 -0
- data/lib/phronomy/execution_cancellation_error.rb +12 -0
- data/lib/phronomy/execution_timeout_error.rb +12 -0
- data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
- data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
- data/lib/phronomy/invocation_context.rb +13 -1
- data/lib/phronomy/llm_adapter/base.rb +2 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
- data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
- data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
- data/lib/phronomy/testing/fake_clock.rb +13 -9
- data/lib/phronomy/tools/agent.rb +5 -5
- data/lib/phronomy/vector_store/async_backend.rb +5 -5
- data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
- data/lib/phronomy/workflow.rb +2 -2
- data/lib/phronomy/workflow_runner.rb +4 -4
- data/scripts/api_snapshot.rb +3 -1
- data/sig/phronomy/agent.rbs +4 -4
- data/sig/phronomy/extensions.rbs +5 -5
- data/sig/phronomy/multi_agent.rbs +1 -3
- data/sig/phronomy/runtime.rbs +34 -7
- data/sig/phronomy/tool.rbs +1 -1
- data/sig/phronomy/workflow.rbs +1 -1
- metadata +11 -5
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +0 -118
|
@@ -6,6 +6,9 @@ module Phronomy
|
|
|
6
6
|
class Orchestrator < Agent::Base
|
|
7
7
|
agent_definition id: "orchestrator", version: 1
|
|
8
8
|
|
|
9
|
+
ParallelChild = Data.define(:index, :agent, :input, :config)
|
|
10
|
+
private_constant :ParallelChild
|
|
11
|
+
|
|
9
12
|
# Own one live cancellation token shared with this invocation's static
|
|
10
13
|
# children, including when Recovery reconstructs the invocation.
|
|
11
14
|
# @api private
|
|
@@ -29,7 +32,7 @@ module Phronomy
|
|
|
29
32
|
|
|
30
33
|
def self.subagent(name, agent_class, on_error: :raise, inherit_knowledge: true)
|
|
31
34
|
# A subagent Tool is logically asynchronous: ToolInvocation starts the
|
|
32
|
-
# child Agent and resumes when its completion
|
|
35
|
+
# child Agent and resumes when its completion TaskResult settles. It must not
|
|
33
36
|
# occupy an OffloadPool worker while waiting for the child.
|
|
34
37
|
tool_class = Class.new(Phronomy::Tools::Agent) do
|
|
35
38
|
def self.__framework_owned_operation? = true
|
|
@@ -57,7 +60,7 @@ module Phronomy
|
|
|
57
60
|
super(args, cancellation_token: cancellation_token, config: config)
|
|
58
61
|
end
|
|
59
62
|
rescue => error
|
|
60
|
-
Phronomy::
|
|
63
|
+
Phronomy::TaskResult.deferred(name: "subagent-dispatch-failed").tap { |task| task.fail(error) }
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
|
|
@@ -87,7 +90,7 @@ module Phronomy
|
|
|
87
90
|
end
|
|
88
91
|
end
|
|
89
92
|
source = agent.invoke_async(input, config: task_config)
|
|
90
|
-
result_task = Phronomy::
|
|
93
|
+
result_task = Phronomy::TaskResult.deferred(
|
|
91
94
|
name: "subagent-tool-#{name}"
|
|
92
95
|
)
|
|
93
96
|
source.on_complete do |result, error|
|
|
@@ -99,7 +102,7 @@ module Phronomy
|
|
|
99
102
|
end
|
|
100
103
|
result_task
|
|
101
104
|
rescue => error
|
|
102
|
-
result_task ||= Phronomy::
|
|
105
|
+
result_task ||= Phronomy::TaskResult.deferred(
|
|
103
106
|
name: "subagent-tool-#{name}"
|
|
104
107
|
)
|
|
105
108
|
(on_error == :raise) ? result_task.fail(error) : result_task.complete(nil)
|
|
@@ -160,73 +163,38 @@ module Phronomy
|
|
|
160
163
|
inherit_knowledge: true
|
|
161
164
|
)
|
|
162
165
|
validate_parallel_options!(tasks, max_concurrency, on_error)
|
|
163
|
-
return Phronomy::Task.deferred(name: "fan-out-empty").tap { |task| task.complete([]) } if tasks.empty?
|
|
164
|
-
|
|
165
166
|
children = build_fan_out_children(
|
|
166
167
|
tasks,
|
|
167
|
-
cancellation_token:
|
|
168
|
-
invocation_context:
|
|
169
|
-
inherit_knowledge: inherit_knowledge
|
|
170
|
-
)
|
|
171
|
-
invocation = FanOutInvocation.new(
|
|
172
|
-
children: children,
|
|
173
|
-
max_concurrency: max_concurrency || children.length,
|
|
174
|
-
on_error: on_error
|
|
175
|
-
)
|
|
176
|
-
effective_token = cancellation_token || invocation_context&.cancellation_token
|
|
177
|
-
FanOutSessionBuilder.start(
|
|
178
|
-
invocation: invocation,
|
|
179
|
-
timeout: timeout,
|
|
180
|
-
cancellation_token: effective_token
|
|
181
|
-
)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def fan_out(
|
|
185
|
-
agent:,
|
|
186
|
-
inputs:,
|
|
187
|
-
config: {},
|
|
188
|
-
max_concurrency: nil,
|
|
189
|
-
on_error: :raise,
|
|
190
|
-
timeout: nil,
|
|
191
|
-
cancellation_token: nil,
|
|
192
|
-
invocation_context: nil,
|
|
193
|
-
inherit_knowledge: true
|
|
194
|
-
)
|
|
195
|
-
dispatch_parallel(
|
|
196
|
-
*inputs.map do |input|
|
|
197
|
-
{agent: agent, input: input, config: config}
|
|
198
|
-
end,
|
|
199
|
-
max_concurrency: max_concurrency,
|
|
200
|
-
on_error: on_error,
|
|
201
|
-
timeout: timeout,
|
|
202
|
-
cancellation_token: cancellation_token,
|
|
203
|
-
invocation_context: invocation_context,
|
|
168
|
+
cancellation_token: nil,
|
|
169
|
+
invocation_context: nil,
|
|
204
170
|
inherit_knowledge: inherit_knowledge
|
|
205
171
|
)
|
|
206
|
-
|
|
172
|
+
Phronomy::Execution.send(:__run_async, children,
|
|
173
|
+
timeout: timeout, cancellation_token: cancellation_token,
|
|
174
|
+
invocation_context: invocation_context, concurrency_limit: max_concurrency) do |child, execution|
|
|
175
|
+
# Preserve a child-specific context as well as this dispatch's controls.
|
|
176
|
+
# Agent admission adds that context's constraints to the supplied token.
|
|
177
|
+
binding = Phronomy::Execution.__operation_binding(
|
|
178
|
+
invocation_context: execution.invocation_context,
|
|
179
|
+
cancellation_token: child.config[:cancellation_token]
|
|
180
|
+
)
|
|
181
|
+
child_context = child.config[:invocation_context] ||
|
|
182
|
+
execution.invocation_context.merge(parent_task_id: invocation_context&.task_id)
|
|
183
|
+
begin
|
|
184
|
+
source = child.agent.invoke_async(child.input,
|
|
185
|
+
config: child.config.merge(cancellation_token: binding.token),
|
|
186
|
+
invocation_context: child_context)
|
|
187
|
+
binding.track(source)
|
|
188
|
+
rescue
|
|
189
|
+
binding.close
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
192
|
+
end.map do |outcomes|
|
|
193
|
+
failure = outcomes.find { |outcome| outcome.status != :completed }
|
|
194
|
+
raise failure.error if failure && on_error == :raise
|
|
207
195
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
inputs:,
|
|
211
|
-
config: {},
|
|
212
|
-
max_concurrency: nil,
|
|
213
|
-
on_error: :raise,
|
|
214
|
-
timeout: nil,
|
|
215
|
-
cancellation_token: nil,
|
|
216
|
-
invocation_context: nil,
|
|
217
|
-
inherit_knowledge: true
|
|
218
|
-
)
|
|
219
|
-
dispatch_parallel_async(
|
|
220
|
-
*inputs.map do |input|
|
|
221
|
-
{agent: agent, input: input, config: config}
|
|
222
|
-
end,
|
|
223
|
-
max_concurrency: max_concurrency,
|
|
224
|
-
on_error: on_error,
|
|
225
|
-
timeout: timeout,
|
|
226
|
-
cancellation_token: cancellation_token,
|
|
227
|
-
invocation_context: invocation_context,
|
|
228
|
-
inherit_knowledge: inherit_knowledge
|
|
229
|
-
)
|
|
196
|
+
outcomes.map { |outcome| (outcome.status == :completed) ? outcome.value : nil }
|
|
197
|
+
end
|
|
230
198
|
end
|
|
231
199
|
|
|
232
200
|
def subagent(
|
|
@@ -366,7 +334,7 @@ module Phronomy
|
|
|
366
334
|
knowledge_snapshot: knowledge_snapshot
|
|
367
335
|
)
|
|
368
336
|
|
|
369
|
-
|
|
337
|
+
ParallelChild.new(
|
|
370
338
|
index: index,
|
|
371
339
|
agent: child_agent,
|
|
372
340
|
input: task.fetch(:input),
|
|
@@ -483,14 +483,14 @@ module Phronomy
|
|
|
483
483
|
param :description, type: :string, desc: "Worker task"
|
|
484
484
|
param :metadata, type: :string, desc: "Optional metadata", required: false
|
|
485
485
|
else
|
|
486
|
-
param :summary, type: :string, desc: "
|
|
486
|
+
param :summary, type: :string, desc: "TaskResult summary", required: false
|
|
487
487
|
end
|
|
488
488
|
define_method(:call_async) do |args, cancellation_token: nil, config: {}|
|
|
489
489
|
validated, schema_error = send(:validate_and_coerce, args)
|
|
490
490
|
raise Phronomy::ToolError, schema_error if schema_error
|
|
491
491
|
execute_async(**validated, cancellation_token: cancellation_token, config: config)
|
|
492
492
|
rescue => error
|
|
493
|
-
Phronomy::
|
|
493
|
+
Phronomy::TaskResult.deferred(name: "team-operation-failed").tap { |task| task.fail(error) }
|
|
494
494
|
end
|
|
495
495
|
define_method(:execute_async) do |config: {}, cancellation_token: nil, **arguments|
|
|
496
496
|
key = config.fetch(:phronomy_tool_invocation_id)
|
|
@@ -542,7 +542,7 @@ module Phronomy
|
|
|
542
542
|
raise Phronomy::ConfigurationError, "Cannot enqueue after finalize" if metadata["finalized"]
|
|
543
543
|
task = {"id" => Digest::SHA256.hexdigest(entry_id)[0, 32], "description" => values.fetch("description"), "metadata" => values["metadata"]}
|
|
544
544
|
tasks << task
|
|
545
|
-
output = "
|
|
545
|
+
output = "TaskResult ##{tasks.length} enqueued: #{task.fetch("description")}"
|
|
546
546
|
else
|
|
547
547
|
output = "Finalized. #{tasks.size} task(s) enqueued. #{values["summary"]}".strip
|
|
548
548
|
metadata["finalized"] = true
|
|
@@ -17,8 +17,10 @@ module Phronomy
|
|
|
17
17
|
Score:
|
|
18
18
|
PROMPT
|
|
19
19
|
|
|
20
|
-
def initialize(model:, prompt_template: DEFAULT_PROMPT, raise_on_error: false)
|
|
20
|
+
def initialize(model:, provider: nil, assume_model_exists: false, prompt_template: DEFAULT_PROMPT, raise_on_error: false)
|
|
21
21
|
@model = model
|
|
22
|
+
@provider = provider
|
|
23
|
+
@assume_model_exists = assume_model_exists
|
|
22
24
|
@prompt_template = prompt_template
|
|
23
25
|
@raise_on_error = raise_on_error
|
|
24
26
|
end
|
|
@@ -31,8 +33,8 @@ module Phronomy
|
|
|
31
33
|
actual: actual.to_s
|
|
32
34
|
)
|
|
33
35
|
response = Phronomy::Runtime.instance.offload.submit do
|
|
34
|
-
RubyLLM.chat(model: @model).ask(prompt)
|
|
35
|
-
end.
|
|
36
|
+
RubyLLM.chat(model: @model, provider: @provider, assume_model_exists: @assume_model_exists).ask(prompt)
|
|
37
|
+
end.wait_result
|
|
36
38
|
response.content.to_s.strip.scan(/-?\d+\.?\d*/).first.to_f.clamp(0.0, 1.0)
|
|
37
39
|
rescue => error
|
|
38
40
|
raise if @raise_on_error
|
|
@@ -13,10 +13,13 @@ module Phronomy
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def advance(seconds)
|
|
16
|
-
@mutex.synchronize do
|
|
16
|
+
fired = @mutex.synchronize do
|
|
17
17
|
@now += seconds.to_f
|
|
18
|
-
|
|
18
|
+
due, @callbacks = @callbacks.partition { |(t, _)| t <= @now }
|
|
19
|
+
due
|
|
19
20
|
end
|
|
21
|
+
# Match TimerQueue: callbacks may dispose or register timers themselves.
|
|
22
|
+
fired.sort_by { |(t, _)| t }.each { |(_, callback)| callback.call }
|
|
20
23
|
self
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -29,6 +32,14 @@ module Phronomy
|
|
|
29
32
|
at(@now + seconds.to_f, &block)
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
def cancel(callback)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
before = @callbacks.length
|
|
38
|
+
@callbacks.delete_if { |(_, registered)| registered == callback }
|
|
39
|
+
before != @callbacks.length
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def pending_callbacks
|
|
33
44
|
@mutex.synchronize { @callbacks.size }
|
|
34
45
|
end
|
|
@@ -50,13 +61,6 @@ module Phronomy
|
|
|
50
61
|
end
|
|
51
62
|
end
|
|
52
63
|
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def fire_expired_callbacks!
|
|
57
|
-
fired, @callbacks = @callbacks.partition { |(t, _)| t <= @now }
|
|
58
|
-
fired.sort_by { |(t, _)| t }.each { |(_, callback)| callback.call }
|
|
59
|
-
end
|
|
60
64
|
end
|
|
61
65
|
end
|
|
62
66
|
end
|
data/lib/phronomy/tools/agent.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Phronomy
|
|
|
106
106
|
"#{self.class.name} asynchronous execution must return a completion handle"
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
result_task = Phronomy::
|
|
109
|
+
result_task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}")
|
|
110
110
|
source.on_complete do |result, error|
|
|
111
111
|
if error
|
|
112
112
|
settle_async_error(result_task, error)
|
|
@@ -123,7 +123,7 @@ module Phronomy
|
|
|
123
123
|
rescue Phronomy::ToolError, Phronomy::CancellationError => error
|
|
124
124
|
failed_task(error)
|
|
125
125
|
rescue => error
|
|
126
|
-
result_task = Phronomy::
|
|
126
|
+
result_task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}")
|
|
127
127
|
settle_async_error(result_task, error)
|
|
128
128
|
result_task
|
|
129
129
|
end
|
|
@@ -133,7 +133,7 @@ module Phronomy
|
|
|
133
133
|
# Subclasses created by .from_agent and Orchestrator override this method.
|
|
134
134
|
# It deliberately remains private so it is not part of the public Tool API.
|
|
135
135
|
def execute_async(input:, cancellation_token: nil, config: {})
|
|
136
|
-
task = Phronomy::
|
|
136
|
+
task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-fallback")
|
|
137
137
|
begin
|
|
138
138
|
task.complete(execute(input: input, cancellation_token: cancellation_token))
|
|
139
139
|
rescue => error
|
|
@@ -143,7 +143,7 @@ module Phronomy
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def schema_error_task(schema_error)
|
|
146
|
-
task = Phronomy::
|
|
146
|
+
task = Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-schema")
|
|
147
147
|
if self.class.on_schema_error == :raise
|
|
148
148
|
task.fail(Phronomy::ToolError.new(
|
|
149
149
|
"#{self.class.name} schema error: #{schema_error}"
|
|
@@ -155,7 +155,7 @@ module Phronomy
|
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
def failed_task(error)
|
|
158
|
-
Phronomy::
|
|
158
|
+
Phronomy::TaskResult.deferred(name: "agent-tool-#{name}-failed").tap do |task|
|
|
159
159
|
task.fail(error)
|
|
160
160
|
end
|
|
161
161
|
end
|
|
@@ -12,7 +12,7 @@ module Phronomy
|
|
|
12
12
|
# the framework rather than by each backend.
|
|
13
13
|
#
|
|
14
14
|
# A future genuine native-async backend may adapt its completion into a
|
|
15
|
-
# {Phronomy::
|
|
15
|
+
# {Phronomy::TaskResult} without an OffloadPool worker, but native async override is
|
|
16
16
|
# not part of the current backend SPI.
|
|
17
17
|
#
|
|
18
18
|
# @api private
|
|
@@ -24,7 +24,7 @@ module Phronomy
|
|
|
24
24
|
# @param metadata [Hash]
|
|
25
25
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
26
26
|
# @param timeout [Numeric, nil]
|
|
27
|
-
# @return [Phronomy::
|
|
27
|
+
# @return [Phronomy::TaskResult]
|
|
28
28
|
# @api public
|
|
29
29
|
def add_async(id:, embedding:, metadata: {}, cancellation_token: nil, timeout: nil)
|
|
30
30
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -42,7 +42,7 @@ module Phronomy
|
|
|
42
42
|
# @param k [Integer]
|
|
43
43
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
44
44
|
# @param timeout [Numeric, nil]
|
|
45
|
-
# @return [Phronomy::
|
|
45
|
+
# @return [Phronomy::TaskResult]
|
|
46
46
|
# @api public
|
|
47
47
|
def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
48
48
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -59,7 +59,7 @@ module Phronomy
|
|
|
59
59
|
# @param id [String]
|
|
60
60
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
61
61
|
# @param timeout [Numeric, nil]
|
|
62
|
-
# @return [Phronomy::
|
|
62
|
+
# @return [Phronomy::TaskResult]
|
|
63
63
|
# @api public
|
|
64
64
|
def remove_async(id:, cancellation_token: nil, timeout: nil)
|
|
65
65
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -75,7 +75,7 @@ module Phronomy
|
|
|
75
75
|
#
|
|
76
76
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
77
77
|
# @param timeout [Numeric, nil]
|
|
78
|
-
# @return [Phronomy::
|
|
78
|
+
# @return [Phronomy::TaskResult]
|
|
79
79
|
# @api public
|
|
80
80
|
def clear_async(cancellation_token: nil, timeout: nil)
|
|
81
81
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -7,7 +7,7 @@ module Phronomy
|
|
|
7
7
|
#
|
|
8
8
|
# Concrete implementations override {#embed}. Phronomy owns the async
|
|
9
9
|
# bridge: {#embed_async} routes the synchronous implementation through the
|
|
10
|
-
# bounded OffloadPool and returns a {Phronomy::
|
|
10
|
+
# bounded OffloadPool and returns a {Phronomy::TaskResult}.
|
|
11
11
|
#
|
|
12
12
|
# @api public
|
|
13
13
|
class Base
|
|
@@ -27,7 +27,7 @@ module Phronomy
|
|
|
27
27
|
# @param text [String]
|
|
28
28
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
29
29
|
# @param timeout [Numeric, nil] operation-wide submit timeout
|
|
30
|
-
# @return [Phronomy::
|
|
30
|
+
# @return [Phronomy::TaskResult]
|
|
31
31
|
# @api public
|
|
32
32
|
def embed_async(text, cancellation_token = nil, timeout: nil)
|
|
33
33
|
Phronomy::Runtime.instance.offload.submit(
|
data/lib/phronomy/version.rb
CHANGED
|
@@ -139,7 +139,7 @@ module Phronomy
|
|
|
139
139
|
machine.context,
|
|
140
140
|
machine.current_event
|
|
141
141
|
)
|
|
142
|
-
if result.is_a?(Phronomy::
|
|
142
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
143
143
|
raise Phronomy::InvalidAsyncTransitionActionError,
|
|
144
144
|
transition_task_error_message(metadata)
|
|
145
145
|
end
|
|
@@ -151,9 +151,9 @@ module Phronomy
|
|
|
151
151
|
def build_entry_callback(callable, state_name)
|
|
152
152
|
->(machine) {
|
|
153
153
|
result = callable.call(machine.context)
|
|
154
|
-
if result.is_a?(Phronomy::
|
|
154
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
155
155
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
156
|
-
"Entry action for state #{state_name.inspect} returned Phronomy::
|
|
156
|
+
"Entry action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
|
|
157
157
|
"Start the asynchronous operation, register its callback/listener, " \
|
|
158
158
|
"and return the WorkflowContext or nil."
|
|
159
159
|
end
|
|
@@ -164,9 +164,9 @@ module Phronomy
|
|
|
164
164
|
def build_exit_callback(callable, state_name)
|
|
165
165
|
->(machine) {
|
|
166
166
|
result = callable.call(machine.context)
|
|
167
|
-
if result.is_a?(Phronomy::
|
|
167
|
+
if result.is_a?(Phronomy::TaskResult)
|
|
168
168
|
raise Phronomy::InvalidAsyncEntryActionError,
|
|
169
|
-
"Exit action for state #{state_name.inspect} returned Phronomy::
|
|
169
|
+
"Exit action for state #{state_name.inspect} returned Phronomy::TaskResult. " \
|
|
170
170
|
"Exit actions are synchronous Run-to-Completion callbacks."
|
|
171
171
|
end
|
|
172
172
|
}
|
|
@@ -196,7 +196,7 @@ module Phronomy
|
|
|
196
196
|
def transition_task_error_message(metadata)
|
|
197
197
|
"Transition action " \
|
|
198
198
|
"#{metadata[:from].inspect} --#{metadata[:event].inspect}--> " \
|
|
199
|
-
"#{metadata[:to].inspect} returned Phronomy::
|
|
199
|
+
"#{metadata[:to].inspect} returned Phronomy::TaskResult. " \
|
|
200
200
|
"Start the asynchronous operation, register its callback/listener, " \
|
|
201
201
|
"and return the WorkflowContext or nil."
|
|
202
202
|
end
|
data/lib/phronomy/workflow.rb
CHANGED
|
@@ -93,7 +93,7 @@ module Phronomy
|
|
|
93
93
|
#
|
|
94
94
|
# Entry actions are synchronous Run-to-Completion callbacks. To start
|
|
95
95
|
# asynchronous work, register its listener/callback inside the action and
|
|
96
|
-
# return the context or nil. Returning Phronomy::
|
|
96
|
+
# return the context or nil. Returning Phronomy::TaskResult is an error.
|
|
97
97
|
def state(name, action: nil)
|
|
98
98
|
@declared_states << name
|
|
99
99
|
entry(name, action) if action
|
|
@@ -117,7 +117,7 @@ module Phronomy
|
|
|
117
117
|
# Transition actions are synchronous Run-to-Completion callbacks executed
|
|
118
118
|
# after the source exit callbacks and before the target entry callbacks.
|
|
119
119
|
# They may start asynchronous work and register listeners, but returning
|
|
120
|
-
# Phronomy::
|
|
120
|
+
# Phronomy::TaskResult is an error; completion must arrive as a later event.
|
|
121
121
|
def transition(from:, to:, guard: nil, on: nil, action: nil)
|
|
122
122
|
destination = (to == :__finish__) ? FINISH : to
|
|
123
123
|
@transitions << {
|
|
@@ -216,7 +216,7 @@ module Phronomy
|
|
|
216
216
|
def start_new_execution(input, config, stable_observer: nil)
|
|
217
217
|
reject_legacy_workflow_identity_key!(config)
|
|
218
218
|
runtime = Phronomy::Runtime.instance
|
|
219
|
-
result_task = Phronomy::
|
|
219
|
+
result_task = Phronomy::TaskResult.deferred(name: "workflow:preparing")
|
|
220
220
|
explicit_workflow_instance_id = !config[:workflow_instance_id].nil?
|
|
221
221
|
workflow_instance_id = (config[:workflow_instance_id] || SecureRandom.uuid).to_s.freeze
|
|
222
222
|
Phronomy::Tracing::Automatic.observe_task(
|
|
@@ -247,7 +247,7 @@ module Phronomy
|
|
|
247
247
|
def start_resume_execution(state, input:, event_name:, current_phase:)
|
|
248
248
|
runtime = Phronomy::Runtime.instance
|
|
249
249
|
workflow_instance_id = state.workflow_instance_id.to_s.freeze
|
|
250
|
-
result_task = Phronomy::
|
|
250
|
+
result_task = Phronomy::TaskResult.deferred(name: "workflow-resume:#{workflow_instance_id}")
|
|
251
251
|
Phronomy::Tracing::Automatic.observe_task(
|
|
252
252
|
result_task,
|
|
253
253
|
"workflow.execution",
|
|
@@ -582,7 +582,7 @@ module Phronomy
|
|
|
582
582
|
fsm_session_id: session.id
|
|
583
583
|
)
|
|
584
584
|
|
|
585
|
-
source_task = Phronomy::
|
|
585
|
+
source_task = Phronomy::TaskResult.deferred(name: "workflow-source:#{session.id}")
|
|
586
586
|
source_task.on_complete do |result, error|
|
|
587
587
|
finalize_execution(
|
|
588
588
|
execution: execution,
|
|
@@ -797,7 +797,7 @@ module Phronomy
|
|
|
797
797
|
end
|
|
798
798
|
|
|
799
799
|
def failed_task(name, error)
|
|
800
|
-
task = Phronomy::
|
|
800
|
+
task = Phronomy::TaskResult.deferred(name: name)
|
|
801
801
|
fail_task(task, error)
|
|
802
802
|
task
|
|
803
803
|
end
|
data/scripts/api_snapshot.rb
CHANGED
|
@@ -70,7 +70,9 @@ def snapshot_entry(klass)
|
|
|
70
70
|
"public_instance_methods" => own_methods
|
|
71
71
|
}
|
|
72
72
|
else
|
|
73
|
-
|
|
73
|
+
internal_context_methods = (klass == Phronomy::InvocationContext) ?
|
|
74
|
+
%i[__bind_execution __execution_scope] : []
|
|
75
|
+
instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS - internal_context_methods).sort
|
|
74
76
|
class_methods = (klass.public_methods(false) - BASELINE_CLASS_METHODS).sort
|
|
75
77
|
{
|
|
76
78
|
"name" => klass.name,
|
data/sig/phronomy/agent.rbs
CHANGED
|
@@ -213,18 +213,18 @@ module Phronomy
|
|
|
213
213
|
# Public operation methods no longer accept per-invocation event listeners
|
|
214
214
|
# or event-listener blocks.
|
|
215
215
|
def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
|
|
216
|
-
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) ->
|
|
216
|
+
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
|
|
217
217
|
def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
|
|
218
|
-
def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) ->
|
|
218
|
+
def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
|
|
219
219
|
|
|
220
220
|
def approve: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> untyped
|
|
221
|
-
def approve_async: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) ->
|
|
221
|
+
def approve_async: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> TaskResult[untyped]
|
|
222
222
|
|
|
223
223
|
# Resolves a factual Recovery ambiguity. result: is meaningful for
|
|
224
224
|
# :succeeded; error: is required for :failed; neither is used by
|
|
225
225
|
# :not_performed.
|
|
226
226
|
def resolve: (String execution_id, expected_execution_revision: Integer, subject: Hash[Symbol, untyped], outcome: Symbol, ?result: untyped, ?error: Exception) -> untyped
|
|
227
|
-
def resolve_async: (String execution_id, expected_execution_revision: Integer, subject: Hash[Symbol, untyped], outcome: Symbol, ?result: untyped, ?error: Exception) ->
|
|
227
|
+
def resolve_async: (String execution_id, expected_execution_revision: Integer, subject: Hash[Symbol, untyped], outcome: Symbol, ?result: untyped, ?error: Exception) -> TaskResult[untyped]
|
|
228
228
|
|
|
229
229
|
def transcript: () -> Array[untyped]
|
|
230
230
|
def add_knowledge: (untyped content, ?metadata: Hash[untyped, untyped]) -> self
|
data/sig/phronomy/extensions.rbs
CHANGED
|
@@ -28,16 +28,16 @@ module Phronomy
|
|
|
28
28
|
def clear: () -> untyped
|
|
29
29
|
def size: () -> Integer
|
|
30
30
|
|
|
31
|
-
def add_async: (id: String, embedding: Array[Float], ?metadata: Hash[untyped, untyped], ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) ->
|
|
32
|
-
def search_async: (query_embedding: Array[Float], ?k: Integer, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) ->
|
|
33
|
-
def remove_async: (id: String, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) ->
|
|
34
|
-
def clear_async: (?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) ->
|
|
31
|
+
def add_async: (id: String, embedding: Array[Float], ?metadata: Hash[untyped, untyped], ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> TaskResult[untyped]
|
|
32
|
+
def search_async: (query_embedding: Array[Float], ?k: Integer, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> TaskResult[Array[Hash[Symbol, untyped]]]
|
|
33
|
+
def remove_async: (id: String, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> TaskResult[untyped]
|
|
34
|
+
def clear_async: (?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> TaskResult[untyped]
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
module Embeddings
|
|
38
38
|
class Base
|
|
39
39
|
def embed: (String text, ?Concurrency::CancellationToken? cancellation_token) -> Array[Float]
|
|
40
|
-
def embed_async: (String text, ?Concurrency::CancellationToken? cancellation_token, ?timeout: Numeric?) ->
|
|
40
|
+
def embed_async: (String text, ?Concurrency::CancellationToken? cancellation_token, ?timeout: Numeric?) -> TaskResult[Array[Float]]
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -5,9 +5,7 @@ module Phronomy
|
|
|
5
5
|
def self.registered_subagents: () -> Hash[Symbol | String, untyped]
|
|
6
6
|
def resume: (String execution_id, ?config: Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
7
7
|
def dispatch_parallel: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
|
|
8
|
-
def dispatch_parallel_async: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> ::Phronomy::
|
|
9
|
-
def fan_out: (agent: singleton(::Phronomy::Agent::Base), inputs: Array[untyped], ?config: Hash[Symbol, untyped], ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> Array[untyped]
|
|
10
|
-
def fan_out_async: (agent: singleton(::Phronomy::Agent::Base), inputs: Array[untyped], ?config: Hash[Symbol, untyped], ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> ::Phronomy::Task[untyped]
|
|
8
|
+
def dispatch_parallel_async: (*Hash[Symbol, untyped] tasks, ?max_concurrency: Integer?, ?on_error: Symbol, ?timeout: Numeric?, ?cancellation_token: untyped, ?invocation_context: untyped, ?inherit_knowledge: bool) -> ::Phronomy::TaskResult[untyped]
|
|
11
9
|
end
|
|
12
10
|
|
|
13
11
|
class TeamCoordinator
|
data/sig/phronomy/runtime.rbs
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
module Phronomy
|
|
2
|
-
class
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
class TaskResult[A]
|
|
3
|
+
class Outcome[V]
|
|
4
|
+
attr_reader index: Integer
|
|
5
|
+
attr_reader status: (:completed | :failed | :cancelled | :unfinished)
|
|
6
|
+
attr_reader value: V?
|
|
7
|
+
attr_reader error: Exception?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.completed: [V] (V value, ?name: String?) -> TaskResult[V]
|
|
11
|
+
| (?name: String?) -> TaskResult[nil]
|
|
12
|
+
def self.failed: (Exception error, ?name: String?) -> TaskResult[untyped]
|
|
13
|
+
def self.all_settled: [V] (Array[TaskResult[V]] results) -> TaskResult[Array[Outcome[V]]]
|
|
6
14
|
attr_reader name: String?
|
|
7
|
-
attr_reader parent:
|
|
15
|
+
attr_reader parent: TaskResult[untyped]?
|
|
8
16
|
|
|
9
17
|
def status: () -> (:pending | :completed | :failed | :cancelled)
|
|
10
18
|
def done?: () -> bool
|
|
11
19
|
def alive?: () -> bool
|
|
12
20
|
def wait_result: (?timeout: Numeric?) -> A
|
|
13
21
|
def on_complete: () { (A?, Exception?) -> void } -> self
|
|
14
|
-
def map: [B] () { (A) -> B } ->
|
|
22
|
+
def map: [B] () { (A) -> B } -> TaskResult[B]
|
|
23
|
+
def flat_map: [B] () { (A) -> TaskResult[B] } -> TaskResult[B]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Execution
|
|
27
|
+
attr_reader invocation_context: InvocationContext
|
|
28
|
+
|
|
29
|
+
def self.run_async: [I, V] (Array[I] inputs, ?timeout: Numeric?, ?cancellation_token: Concurrency::CancellationToken?, ?invocation_context: InvocationContext?) { (I, Execution) -> TaskResult[V] } -> TaskResult[Array[TaskResult::Outcome[V]]]
|
|
30
|
+
def self.run: [I, V] (Array[I] inputs, ?timeout: Numeric?, ?cancellation_token: Concurrency::CancellationToken?, ?invocation_context: InvocationContext?) { (I, Execution) -> TaskResult[V] } -> Array[TaskResult::Outcome[V]]
|
|
31
|
+
def observe: [V] (TaskResult[V] source_result) -> TaskResult[V]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ExecutionTimeoutError < TimeoutError
|
|
35
|
+
attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
|
|
36
|
+
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ExecutionCancellationError < CancellationError
|
|
40
|
+
attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
|
|
41
|
+
def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
|
|
15
42
|
end
|
|
16
43
|
|
|
17
44
|
module Concurrency
|
|
@@ -44,6 +71,6 @@ end
|
|
|
44
71
|
|
|
45
72
|
module Phronomy
|
|
46
73
|
module Blocking
|
|
47
|
-
def self.call_async: [V] (?timeout: Numeric?, ?cancellation_token: Concurrency::CancellationToken?) { () -> V } ->
|
|
74
|
+
def self.call_async: [V] (?timeout: Numeric?, ?cancellation_token: Concurrency::CancellationToken?, ?invocation_context: InvocationContext?) { () -> V } -> TaskResult[V]
|
|
48
75
|
end
|
|
49
76
|
end
|
data/sig/phronomy/tool.rbs
CHANGED
|
@@ -21,7 +21,7 @@ module Phronomy
|
|
|
21
21
|
def name: () -> String
|
|
22
22
|
def params_schema: () -> untyped
|
|
23
23
|
def call: (Hash[untyped, untyped] args, ?cancellation_token: Concurrency::CancellationToken?) -> untyped
|
|
24
|
-
def call_async: (Hash[untyped, untyped] args, ?cancellation_token: Concurrency::CancellationToken?, ?config: Hash[untyped, untyped]) ->
|
|
24
|
+
def call_async: (Hash[untyped, untyped] args, ?cancellation_token: Concurrency::CancellationToken?, ?config: Hash[untyped, untyped]) -> TaskResult[untyped]
|
|
25
25
|
def requires_approval?: () -> untyped
|
|
26
26
|
def tool_origin: () -> Symbol
|
|
27
27
|
def approval_metadata: () -> Hash[untyped, untyped]
|
data/sig/phronomy/workflow.rbs
CHANGED
|
@@ -11,7 +11,7 @@ module Phronomy
|
|
|
11
11
|
def self.define: (Class context_class, ?persistence: Persistence?) { () -> void } -> Workflow
|
|
12
12
|
|
|
13
13
|
def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> untyped
|
|
14
|
-
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) ->
|
|
14
|
+
def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[untyped]
|
|
15
15
|
def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) { (untyped) -> void } -> untyped
|
|
16
16
|
def resume: (state: untyped, ?input: untyped) -> untyped
|
|
17
17
|
def send_event: (state: untyped, event: Symbol, ?input: untyped) -> untyped
|