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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/CONTRIBUTING.md +4 -4
  4. data/README.md +10 -7
  5. data/benchmark/bench_regression.rb +1 -1
  6. data/docs/architecture/agent-context.md +1 -1
  7. data/docs/architecture/persistence.md +2 -2
  8. data/docs/architecture/tracing.md +3 -3
  9. data/docs/async-composition.md +205 -0
  10. data/docs/decisions/010-cooperative-first-concurrency.md +23 -23
  11. data/docs/features.md +12 -9
  12. data/docs/getting-started.md +6 -6
  13. data/docs/migrations/durable-semantic-coordination-v2.md +1 -1
  14. data/docs/persistence-backends.md +1 -1
  15. data/docs/runtime-and-concurrency.md +89 -29
  16. data/lib/phronomy/agent/agent_invocation_session_builder.rb +1 -1
  17. data/lib/phronomy/agent/base.rb +4 -10
  18. data/lib/phronomy/agent/exact_execution.rb +3 -3
  19. data/lib/phronomy/agent/execution_coordinator.rb +20 -7
  20. data/lib/phronomy/agent/phase_machine_builder.rb +2 -2
  21. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +1 -1
  22. data/lib/phronomy/agent/recovery_coordinator/installation.rb +4 -4
  23. data/lib/phronomy/agent/recovery_coordinator.rb +3 -3
  24. data/lib/phronomy/agent/tool_executor.rb +4 -4
  25. data/lib/phronomy/agent/tool_invocation.rb +1 -1
  26. data/lib/phronomy/agent/tool_invocation_session_builder.rb +2 -2
  27. data/lib/phronomy/blocking.rb +15 -8
  28. data/lib/phronomy/engine/concurrency/offload_pool.rb +25 -16
  29. data/lib/phronomy/engine/concurrency/operation_binding.rb +43 -0
  30. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +4 -62
  31. data/lib/phronomy/engine/concurrency/result_collector.rb +99 -0
  32. data/lib/phronomy/engine/concurrency/result_composition.rb +145 -0
  33. data/lib/phronomy/engine/concurrency/subscriptions.rb +68 -0
  34. data/lib/phronomy/engine/event_loop.rb +7 -7
  35. data/lib/phronomy/engine/fsm_session.rb +3 -3
  36. data/lib/phronomy/engine/runtime/timer_queue.rb +11 -0
  37. data/lib/phronomy/engine/{task.rb → task_result.rb} +97 -42
  38. data/lib/phronomy/execution.rb +236 -0
  39. data/lib/phronomy/execution_cancellation_error.rb +12 -0
  40. data/lib/phronomy/execution_timeout_error.rb +12 -0
  41. data/lib/phronomy/invalid_async_entry_action_error.rb +1 -1
  42. data/lib/phronomy/invalid_async_transition_action_error.rb +1 -1
  43. data/lib/phronomy/invalid_async_workflow_action_error.rb +1 -1
  44. data/lib/phronomy/invocation_context.rb +13 -1
  45. data/lib/phronomy/llm_adapter/base.rb +2 -2
  46. data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +3 -3
  47. data/lib/phronomy/multi_agent/orchestrator.rb +35 -67
  48. data/lib/phronomy/multi_agent/team_coordinator.rb +3 -3
  49. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +5 -3
  50. data/lib/phronomy/testing/fake_clock.rb +13 -9
  51. data/lib/phronomy/tools/agent.rb +5 -5
  52. data/lib/phronomy/vector_store/async_backend.rb +5 -5
  53. data/lib/phronomy/vector_store/embeddings/base.rb +2 -2
  54. data/lib/phronomy/version.rb +1 -1
  55. data/lib/phronomy/workflow/phase_machine_builder.rb +6 -6
  56. data/lib/phronomy/workflow.rb +2 -2
  57. data/lib/phronomy/workflow_runner.rb +4 -4
  58. data/scripts/api_snapshot.rb +3 -1
  59. data/sig/phronomy/agent.rbs +4 -4
  60. data/sig/phronomy/extensions.rbs +5 -5
  61. data/sig/phronomy/multi_agent.rbs +1 -3
  62. data/sig/phronomy/runtime.rbs +34 -7
  63. data/sig/phronomy/tool.rbs +1 -1
  64. data/sig/phronomy/workflow.rbs +1 -1
  65. metadata +11 -5
  66. data/lib/phronomy/multi_agent/fan_out_invocation.rb +0 -137
  67. 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 Task settles. It must not
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::Task.deferred(name: "subagent-dispatch-failed").tap { |task| task.fail(error) }
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::Task.deferred(
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::Task.deferred(
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: cancellation_token,
168
- invocation_context: 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
- end
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
- def fan_out_async(
209
- agent:,
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
- FanOutInvocation::Child.new(
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: "Task summary", required: false
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::Task.deferred(name: "team-operation-failed").tap { |task| task.fail(error) }
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 = "Task ##{tasks.length} enqueued: #{task.fetch("description")}"
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.blocking_wait
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
- fire_expired_callbacks!
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
@@ -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::Task.deferred(name: "agent-tool-#{name}")
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::Task.deferred(name: "agent-tool-#{name}")
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::Task.deferred(name: "agent-tool-#{name}-fallback")
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::Task.deferred(name: "agent-tool-#{name}-schema")
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::Task.deferred(name: "agent-tool-#{name}-failed").tap do |task|
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::Task} without an OffloadPool worker, but native async override is
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::Task]
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::Task]
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::Task]
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::Task]
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::Task}.
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::Task]
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(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.25.0"
4
+ VERSION = "0.26.0"
5
5
  end
@@ -139,7 +139,7 @@ module Phronomy
139
139
  machine.context,
140
140
  machine.current_event
141
141
  )
142
- if result.is_a?(Phronomy::Task)
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::Task)
154
+ if result.is_a?(Phronomy::TaskResult)
155
155
  raise Phronomy::InvalidAsyncEntryActionError,
156
- "Entry action for state #{state_name.inspect} returned Phronomy::Task. " \
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::Task)
167
+ if result.is_a?(Phronomy::TaskResult)
168
168
  raise Phronomy::InvalidAsyncEntryActionError,
169
- "Exit action for state #{state_name.inspect} returned Phronomy::Task. " \
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::Task. " \
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
@@ -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::Task is an error.
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::Task is an error; completion must arrive as a later event.
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::Task.deferred(name: "workflow:preparing")
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::Task.deferred(name: "workflow-resume:#{workflow_instance_id}")
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::Task.deferred(name: "workflow-source:#{session.id}")
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::Task.deferred(name: name)
800
+ task = Phronomy::TaskResult.deferred(name: name)
801
801
  fail_task(task, error)
802
802
  task
803
803
  end
@@ -70,7 +70,9 @@ def snapshot_entry(klass)
70
70
  "public_instance_methods" => own_methods
71
71
  }
72
72
  else
73
- instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS).sort
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,
@@ -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) -> Task[Hash[Symbol, 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) -> Task[Hash[Symbol, 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]) -> Task[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) -> Task[untyped]
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
@@ -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?) -> Task[untyped]
32
- def search_async: (query_embedding: Array[Float], ?k: Integer, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[Array[Hash[Symbol, untyped]]]
33
- def remove_async: (id: String, ?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[untyped]
34
- def clear_async: (?cancellation_token: Concurrency::CancellationToken?, ?timeout: Numeric?) -> Task[untyped]
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?) -> Task[Array[Float]]
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::Task[untyped]
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
@@ -1,17 +1,44 @@
1
1
  module Phronomy
2
- class Task[A]
3
- def self.completed: [V] (V value, ?name: String?) -> Task[V]
4
- | (?name: String?) -> Task[nil]
5
- def self.failed: (Exception error, ?name: String?) -> Task[untyped]
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: Task[untyped]?
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 } -> Task[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 } -> Task[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
@@ -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]) -> Task[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]
@@ -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) -> Task[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