phronomy 0.16.0 → 0.18.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +151 -1134
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +69 -1249
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +4 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +26 -6
  10. data/benchmark/bench_tool_schema.rb +2 -35
  11. data/docs/changelog/0.14-and-earlier.md +1137 -0
  12. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  13. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  14. data/docs/decisions/010-cooperative-first-concurrency.md +155 -228
  15. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +2 -2
  16. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  17. data/docs/features.md +87 -0
  18. data/docs/getting-started.md +351 -0
  19. data/docs/migrations/0.15.md +35 -0
  20. data/docs/migrations/0.16.md +43 -0
  21. data/docs/runtime-and-concurrency.md +258 -0
  22. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  23. data/lib/phronomy/agent/agent_invocation.rb +2 -36
  24. data/lib/phronomy/agent/agent_invocation_session_builder.rb +157 -94
  25. data/lib/phronomy/agent/agent_root.rb +1 -2
  26. data/lib/phronomy/agent/async_event_api.rb +3 -3
  27. data/lib/phronomy/agent/base.rb +170 -333
  28. data/lib/phronomy/agent/context/capability/base.rb +177 -298
  29. data/lib/phronomy/agent/context_assembler.rb +65 -29
  30. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +19 -89
  31. data/lib/phronomy/agent/context_plan_validator.rb +0 -33
  32. data/lib/phronomy/agent/execution_coordinator.rb +6 -7
  33. data/lib/phronomy/agent/journal_projection.rb +28 -2
  34. data/lib/phronomy/agent/ruby_llm_materializer.rb +2 -111
  35. data/lib/phronomy/agent/shared_state.rb +48 -138
  36. data/lib/phronomy/agent/token_budget_resolver.rb +5 -4
  37. data/lib/phronomy/agent/tool_executor.rb +29 -71
  38. data/lib/phronomy/agent/tool_invocation.rb +180 -336
  39. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  40. data/lib/phronomy/agent.rb +6 -10
  41. data/lib/phronomy/configuration.rb +4 -171
  42. data/lib/phronomy/diagnostics.rb +12 -41
  43. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  44. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  45. data/lib/phronomy/engine/concurrency/cancellation_token.rb +51 -79
  46. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  47. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  48. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  49. data/lib/phronomy/engine/event_loop.rb +89 -190
  50. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  51. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  52. data/lib/phronomy/engine/runtime.rb +49 -377
  53. data/lib/phronomy/engine/task.rb +136 -277
  54. data/lib/phronomy/llm_adapter/base.rb +14 -14
  55. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  56. data/lib/phronomy/llm_adapter.rb +2 -2
  57. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  58. data/lib/phronomy/metrics.rb +15 -30
  59. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  60. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  61. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  62. data/lib/phronomy/multi_agent/orchestrator.rb +252 -256
  63. data/lib/phronomy/multi_agent/team_coordinator.rb +44 -133
  64. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  65. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  66. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  67. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  68. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  69. data/lib/phronomy/testing/eval/runner.rb +52 -0
  70. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  71. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  72. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  73. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  74. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  75. data/lib/phronomy/testing/eval.rb +9 -0
  76. data/lib/phronomy/testing/fake_clock.rb +6 -53
  77. data/lib/phronomy/testing.rb +2 -6
  78. data/lib/phronomy/tools/agent.rb +141 -6
  79. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  80. data/lib/phronomy/vector_store/base.rb +2 -2
  81. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  82. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  83. data/lib/phronomy/version.rb +1 -1
  84. data/lib/phronomy/workflow_runner.rb +2 -4
  85. data/lib/phronomy.rb +7 -121
  86. data/scripts/api_snapshot.rb +4 -15
  87. metadata +24 -38
  88. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  89. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  90. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  91. data/lib/phronomy/agent/fsm_runtime_adapter.rb +0 -210
  92. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  93. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  94. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  95. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  96. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  97. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  98. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  99. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  100. data/lib/phronomy/engine/task/backend.rb +0 -80
  101. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  102. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  103. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  104. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  105. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  106. data/lib/phronomy/engine/task_group.rb +0 -265
  107. data/lib/phronomy/eval/comparison.rb +0 -47
  108. data/lib/phronomy/eval/dataset.rb +0 -45
  109. data/lib/phronomy/eval/eval_case.rb +0 -17
  110. data/lib/phronomy/eval/eval_result.rb +0 -29
  111. data/lib/phronomy/eval/metrics.rb +0 -66
  112. data/lib/phronomy/eval/runner.rb +0 -94
  113. data/lib/phronomy/eval/scorer/base.rb +0 -22
  114. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  115. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  116. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  117. data/lib/phronomy/eval/scorer.rb +0 -9
  118. data/lib/phronomy/eval.rb +0 -7
  119. data/lib/phronomy/knowledge_source.rb +0 -12
  120. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  121. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
  122. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  123. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -2,96 +2,25 @@
2
2
 
3
3
  module Phronomy
4
4
  module LlmContextWindow
5
- # Raised when a model name is not found in the RubyLLM model registry and
6
- # no explicit context_window was provided.
7
- class UnknownModelError < Phronomy::Error; end
8
-
9
- # Calculates the effective token budget available for conversation history
10
- # and injected knowledge within a single LLM request.
11
- #
12
- # The window is divided as follows:
13
- #
14
- # context_window (total)
15
- # ├─ max_output_tokens (reserved for model output = max_output_tokens)
16
- # ├─ overhead (reserved for system prompt + tool definitions)
17
- # └─ effective_input_limit (available for memory + knowledge)
18
- #
19
- # @example Auto-derive from RubyLLM model registry
20
- # budget = Phronomy::LlmContextWindow::TokenBudget.new(model: "claude-3-5-sonnet-20241022")
21
- #
22
- # @example Explicit values (useful for local / unknown models)
23
- # budget = Phronomy::LlmContextWindow::TokenBudget.new(
24
- # context_window: 32_768,
25
- # max_output_tokens: 4_096
26
- # )
27
- #
28
- # @example With overhead for instructions + tool definitions
29
- # budget = Phronomy::LlmContextWindow::TokenBudget.new(
30
- # model: "gpt-4o",
31
- # overhead: 800
32
- # )
5
+ # Immutable arithmetic value for one resolved model context budget.
6
+ # Model-registry lookup belongs to Agent::TokenBudgetResolver.
33
7
  class TokenBudget
34
- # @return [Integer] total token limit of the model
35
- attr_reader :context_window
36
-
37
- # @return [Integer] tokens reserved for model output
38
- attr_reader :max_output_tokens
8
+ attr_reader :context_window, :max_output_tokens
39
9
 
40
- # @return [Integer] tokens reserved for instructions and tool definitions
41
- attr_reader :overhead
42
-
43
- # @param model [String, nil] model identifier looked up in RubyLLM
44
- # @param context_window [Integer, nil] explicit total token limit
45
- # @param max_output_tokens [Integer, nil] explicit output reservation; when nil
46
- # and model is given, uses max_output_tokens
47
- # @param overhead [Integer] tokens reserved for instructions/tools
48
10
  # @api private
49
- # mutant:disable - multiple genuine equivalent mutations: overhead/context_window/max_output_tokens .to_i vs .to_int vs Integer() vs omitted are equivalent for Integer inputs; (max_output_tokens||0).to_i vs (max_output_tokens).to_i and (||nil).to_i are genuine because nil.to_i==0; overhead:nil default is genuine because nil.to_i==0
50
- def initialize(model: nil, context_window: nil, max_output_tokens: nil, overhead: 0)
51
- @overhead = overhead.to_i
52
-
53
- if context_window
54
- # Explicit values — no registry lookup needed.
55
- @context_window = context_window.to_i
56
- @max_output_tokens = (max_output_tokens || 0).to_i
57
- elsif model
58
- ruby_llm_model = lookup_model!(model)
59
- @context_window = ruby_llm_model.context_window.to_i
60
- @max_output_tokens = (max_output_tokens || ruby_llm_model.max_output_tokens).to_i
61
- else
62
- raise ArgumentError, "Provide either model: or context_window:"
63
- end
11
+ def initialize(context_window:, max_output_tokens:)
12
+ @context_window = Integer(context_window)
13
+ @max_output_tokens = Integer(max_output_tokens)
64
14
  end
65
15
 
66
- # Tokens available for conversation history and knowledge after reservations.
67
- # Always >= 0.
68
- #
69
- # @return [Integer]
70
16
  # @api private
71
17
  def effective_input_limit
72
- [@context_window - @max_output_tokens - @overhead, 0].max
18
+ [@context_window - @max_output_tokens, 0].max
73
19
  end
74
20
 
75
- # Tokens still available after `used` tokens have been allocated.
76
- #
77
- # @param used [Integer] tokens already committed (e.g. from knowledge injection)
78
- # @return [Integer] remaining tokens (always >= 0)
79
21
  # @api private
80
- # mutant:disable - used.to_i vs used vs used.to_int vs Integer(used) are genuine equivalents when used is an Integer; used:nil default is genuine because nil.to_i==0==default 0
81
22
  def available(used: 0)
82
- [effective_input_limit - used.to_i, 0].max
83
- end
84
-
85
- private
86
-
87
- # mutant:disable - raise(UnknownModelError) and raise(UnknownModelError,nil) and raise(UnknownModelError,"Model '#{nil}' not found") in both branches are genuine equivalents (spec checks exception class only, not message text)
88
- def lookup_model!(model_name)
89
- found = RubyLLM.models.find(model_name)
90
- raise UnknownModelError, "Model '#{model_name}' not found in RubyLLM registry" unless found
91
-
92
- found
93
- rescue RubyLLM::ModelNotFoundError
94
- raise UnknownModelError, "Model '#{model_name}' not found in RubyLLM registry"
23
+ [effective_input_limit - Integer(used), 0].max
95
24
  end
96
25
  end
97
26
  end
@@ -1,41 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- # Task-centric observability snapshot (Issue #276, extended in #307).
5
- #
6
- # Collects live metrics from the shared Runtime components
7
- # (BlockingAdapterPool, EventLoop, and Runtime task registry) and returns
8
- # them as a plain Hash so they can be forwarded to any monitoring backend
9
- # (Prometheus, OpenTelemetry, StatsD, etc.).
10
- #
11
- # All metrics are read at the moment {.snapshot} is called; no
12
- # persistent state is held here.
13
- #
14
- # @example Exporting to a metrics endpoint
15
- # data = Phronomy::Metrics.snapshot
16
- # # => { blocking_pool_active: 2, active_agent_tasks: 1, ... }
4
+ # Runtime observability snapshot for the two concurrency boundaries:
5
+ # EventLoop and OffloadPool.
17
6
  module Metrics
18
- # Returns a Hash of current observability metrics.
19
- #
20
- # @return [Hash{Symbol => Numeric}]
21
- # @api public
22
7
  def self.snapshot
23
8
  runtime = Runtime.instance
24
- pool = runtime.blocking_io
25
- el = runtime.event_loop
26
- task_snap = runtime.task_snapshot
9
+ pool = runtime.offload
10
+ event_loop = runtime.event_loop
27
11
 
28
12
  {
29
- blocking_pool_active: pool.active_count,
30
- blocking_pool_queue_length: pool.queue_depth,
31
- blocking_pool_abandoned_total: pool.abandoned_count,
32
- blocking_pool_size: pool.pool_size,
33
- event_loop_queue_depth: el.queue_depth,
34
- event_loop_queue_max_depth: el.max_queue_depth,
35
- event_loop_lag_last_ms: (el.last_lag_seconds * 1000).round(3),
36
- event_loop_lag_max_ms: (el.max_lag_seconds * 1000).round(3),
37
- event_loop_lag_average_ms: (el.average_lag_seconds * 1000).round(3)
38
- }.merge(task_snap)
13
+ offload_pool_active: pool.active_count,
14
+ offload_pool_queue_length: pool.queue_depth,
15
+ offload_pool_abandoned_active: pool.abandoned_active_count,
16
+ offload_pool_abandoned_total: pool.abandoned_count,
17
+ offload_pool_size: pool.pool_size,
18
+ event_loop_queue_depth: event_loop.queue_depth,
19
+ event_loop_queue_max_depth: event_loop.max_queue_depth,
20
+ event_loop_lag_last_ms: (event_loop.last_lag_seconds * 1000).round(3),
21
+ event_loop_lag_max_ms: (event_loop.max_lag_seconds * 1000).round(3),
22
+ event_loop_lag_average_ms: (event_loop.average_lag_seconds * 1000).round(3)
23
+ }
39
24
  end
40
25
  end
41
26
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Phronomy
6
+ module MultiAgent
7
+ # Mutable FSM context for one fan-out operation.
8
+ class FanOutInvocation
9
+ Child = Data.define(:index, :agent, :input, :config, :thread_id)
10
+
11
+ attr_reader :id, :phase, :results
12
+
13
+ def initialize(children:, max_concurrency:, on_error:)
14
+ @id = SecureRandom.uuid.to_s
15
+ @phase = nil
16
+ @children = children
17
+ @max_concurrency = max_concurrency || children.length
18
+ @on_error = on_error
19
+ @pending = children.dup
20
+ @active = {}
21
+ @results = Array.new(children.length)
22
+ @child_errors = Array.new(children.length) # indexed by input order
23
+ @fatal_error = nil # driver_failed / timeout / cancel
24
+ @cancelled = false
25
+ @timed_out = false
26
+ @session_id = nil
27
+ end
28
+
29
+ def set_graph_metadata(thread_id: nil, phase: nil)
30
+ @session_id = thread_id if thread_id
31
+ @phase = phase
32
+ end
33
+
34
+ def handle_fsm_event(event)
35
+ case event.type
36
+ when :child_completed
37
+ payload = event.payload
38
+ index = payload.fetch(:index)
39
+ @active.delete(index)
40
+ child_error = payload[:error]
41
+ if child_error
42
+ if @on_error == :raise
43
+ @child_errors[index] = child_error
44
+ # Signal cancellation so other children can stop early, but continue
45
+ # waiting for all active children to respond so we can return the
46
+ # first error in INPUT ORDER (not arrival order).
47
+ cancel_active_children! unless @child_errors.any?(&:itself)
48
+ end
49
+ else
50
+ @results[index] = payload[:result]
51
+ end
52
+ true
53
+ when :driver_failed
54
+ @fatal_error ||= event.payload.fetch(:error)
55
+ cancel_active_children!
56
+ true
57
+ when :timeout
58
+ @timed_out = true
59
+ @fatal_error ||= Phronomy::TimeoutError.new(event.payload.fetch(:message))
60
+ cancel_active_children!
61
+ true
62
+ when :cancel
63
+ @cancelled = true
64
+ @fatal_error ||= Phronomy::CancellationError.new("fan-out cancelled")
65
+ cancel_active_children!
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def start_available!(runtime)
73
+ return self if @fatal_error
74
+
75
+ while @active.length < @max_concurrency && (child = @pending.shift)
76
+ child_config = build_child_config(child.config)
77
+ handle = child.agent.invoke_async(
78
+ child.input,
79
+ config: child_config,
80
+ thread_id: child.thread_id
81
+ )
82
+ @active[child.index] = {handle: handle, token: child_config[:cancellation_token]}
83
+ # [child.index].each creates a block parameter with a unique binding
84
+ # per iteration, avoiding the while-loop variable capture problem.
85
+ [child.index].each do |captured_index|
86
+ handle.on_complete do |result, error|
87
+ runtime.event_loop.post_to_session(
88
+ Phronomy::Event.new(
89
+ type: :child_completed,
90
+ target_id: @id,
91
+ payload: {index: captured_index, result: result, error: error}
92
+ )
93
+ )
94
+ end
95
+ end
96
+ end
97
+ self
98
+ rescue => caught
99
+ @fatal_error ||= caught
100
+ cancel_active_children!
101
+ runtime.event_loop.post_to_session(
102
+ Phronomy::Event.new(
103
+ type: :driver_failed,
104
+ target_id: @id,
105
+ payload: {error: caught}
106
+ )
107
+ )
108
+ self
109
+ end
110
+
111
+ def completed?
112
+ !failed? && @pending.empty? && @active.empty?
113
+ end
114
+
115
+ def failed?
116
+ return true if @fatal_error
117
+ @on_error == :raise && @pending.empty? && @active.empty? && @child_errors.any?(&:itself)
118
+ end
119
+
120
+ def error
121
+ @fatal_error || @child_errors.find(&:itself)
122
+ end
123
+
124
+ def timed_out?
125
+ @timed_out
126
+ end
127
+
128
+ def cancelled?
129
+ @cancelled
130
+ end
131
+
132
+ private
133
+
134
+ def build_child_config(config)
135
+ config.dup
136
+ end
137
+
138
+ def cancel_active_children!
139
+ @pending.clear
140
+ @active.each_value do |entry|
141
+ entry[:token]&.cancel!
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "state_machines"
4
+
5
+ module Phronomy
6
+ module MultiAgent
7
+ # Builds and registers a fan-out coordination FSMSession.
8
+ class FanOutSessionBuilder
9
+ AUTO_STATE_SET = {idle: true}.freeze
10
+ DECLARED_STATES = %i[idle running completed failed timed_out cancelled].freeze
11
+ WAIT_STATES = [].freeze
12
+
13
+ def self.start(
14
+ invocation:,
15
+ timeout: nil,
16
+ cancellation_token: nil,
17
+ runtime: Phronomy::Runtime.instance
18
+ )
19
+ result = Phronomy::Task.deferred(name: "fan-out:#{invocation.id}")
20
+
21
+ if cancellation_token&.cancelled?
22
+ result.fail(Phronomy::CancellationError.new("fan-out cancelled"))
23
+ return result
24
+ end
25
+
26
+ source = Phronomy::Task.deferred(name: "fan-out-source:#{invocation.id}")
27
+ source.on_complete do |context, error|
28
+ if error
29
+ result.fail(error)
30
+ elsif context.error
31
+ result.fail(context.error)
32
+ else
33
+ result.complete(context.results)
34
+ end
35
+ end
36
+
37
+ session = build(invocation: invocation, runtime: runtime)
38
+ runtime.event_loop.register(session, completion: source)
39
+
40
+ if timeout
41
+ runtime.timer_queue.schedule(seconds: timeout) do
42
+ runtime.event_loop.post_to_session(
43
+ Phronomy::Event.new(
44
+ type: :timeout,
45
+ target_id: invocation.id,
46
+ payload: {message: "dispatch_parallel timed out after #{timeout}s"}
47
+ )
48
+ )
49
+ end
50
+ end
51
+
52
+ cancellation_token&.on_cancel do
53
+ runtime.event_loop.post_to_session(
54
+ Phronomy::Event.new(type: :cancel, target_id: invocation.id, payload: nil)
55
+ )
56
+ end
57
+
58
+ result
59
+ rescue => error
60
+ result ||= Phronomy::Task.deferred(name: "fan-out:registration")
61
+ result.fail(error)
62
+ result
63
+ end
64
+
65
+ def self.build(invocation:, runtime:)
66
+ phase_machine = build_phase_machine(runtime)
67
+ external_events = {
68
+ child_completed: [{from: :running, to: :running, guard: nil}],
69
+ driver_failed: [{from: :running, to: :failed, guard: nil}],
70
+ timeout: [{from: :running, to: :timed_out, guard: nil}],
71
+ cancel: [{from: :running, to: :cancelled, guard: nil}]
72
+ }
73
+
74
+ Phronomy::FSMSession.new(
75
+ id: invocation.id,
76
+ context: invocation,
77
+ entry_point: :idle,
78
+ entry_actions: {},
79
+ auto_state_set: AUTO_STATE_SET,
80
+ declared_states: DECLARED_STATES,
81
+ wait_state_names: WAIT_STATES,
82
+ external_events: external_events,
83
+ phase_machine_class: phase_machine,
84
+ recursion_limit: 10_000,
85
+ event_loop: runtime.event_loop
86
+ )
87
+ end
88
+ private_class_method :build
89
+
90
+ def self.build_phase_machine(runtime)
91
+ Class.new do
92
+ attr_accessor :context, :current_event
93
+
94
+ state_machine :phase, initial: :idle do
95
+ state :idle
96
+ state :running
97
+ state :completed
98
+ state :failed
99
+ state :timed_out
100
+ state :cancelled
101
+
102
+ event :state_completed do
103
+ transition idle: :running
104
+ end
105
+
106
+ event :child_completed do
107
+ transition running: :failed, if: ->(m) { m.context&.failed? }
108
+ transition running: :completed, if: ->(m) { m.context&.completed? }
109
+ transition running: :running
110
+ end
111
+
112
+ event(:driver_failed) { transition running: :failed }
113
+ event(:timeout) { transition running: :timed_out }
114
+ event(:cancel) { transition running: :cancelled }
115
+
116
+ after_transition to: :running do |machine|
117
+ machine.context.start_available!(runtime)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ private_class_method :build_phase_machine
123
+ end
124
+ end
125
+ end
@@ -42,6 +42,7 @@ module Phronomy
42
42
  Class.new(Phronomy::Agent::Context::Capability::Base) do
43
43
  tool_name tn
44
44
  description desc
45
+ execution_mode :cooperative
45
46
  define_method(:execute) { sentinel_value }
46
47
  end
47
48
  end