phronomy 0.28.0 → 0.29.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -5
  3. data/docs/application-recipes.md +229 -0
  4. data/docs/architecture/dx05-agent-scope.md +119 -0
  5. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  6. data/docs/architecture/error-api-review.md +245 -0
  7. data/docs/failure-reason-codes.md +45 -0
  8. data/docs/features.md +4 -1
  9. data/docs/getting-started.md +44 -43
  10. data/docs/ordered-event-delivery.md +54 -0
  11. data/docs/workflow-completion.md +92 -0
  12. data/gemfiles/http_minimum.gemfile +5 -0
  13. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  14. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  15. data/lib/phronomy/agent/api/agent.rb +13 -0
  16. data/lib/phronomy/agent/async_event_api.rb +111 -87
  17. data/lib/phronomy/agent/base.rb +74 -43
  18. data/lib/phronomy/agent/before_llm_input.rb +7 -1
  19. data/lib/phronomy/agent/execution/exact_execution.rb +11 -9
  20. data/lib/phronomy/agent/execution/execution_coordinator.rb +16 -13
  21. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -1
  22. data/lib/phronomy/agent/execution/initial_preparation.rb +1 -4
  23. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +1 -4
  24. data/lib/phronomy/agent/filterable.rb +21 -15
  25. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  26. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  27. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  28. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
  29. data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
  30. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
  31. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  32. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +3 -3
  33. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +7 -7
  34. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
  35. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +5 -9
  36. data/lib/phronomy/agent/recovery/recovery_support.rb +2 -8
  37. data/lib/phronomy/agent/stream_callback_error.rb +2 -2
  38. data/lib/phronomy/common/error.rb +65 -1
  39. data/lib/phronomy/content_store/base.rb +7 -2
  40. data/lib/phronomy/context/assembly.rb +11 -18
  41. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  42. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +5 -1
  43. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  44. data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
  45. data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
  46. data/lib/phronomy/execution/task_result.rb +7 -2
  47. data/lib/phronomy/filter/filter_block_error.rb +2 -2
  48. data/lib/phronomy/generation/low_confidence_error.rb +2 -2
  49. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  50. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
  51. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  52. data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
  53. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  54. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  55. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  56. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
  57. data/lib/phronomy/tool/base.rb +25 -23
  58. data/lib/phronomy/tracing/base.rb +35 -6
  59. data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
  60. data/lib/phronomy/version.rb +1 -1
  61. data/lib/phronomy/workflow/execution/workflow.rb +30 -2
  62. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
  63. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
  64. data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
  65. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  66. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  67. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  68. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
  69. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
  70. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  71. data/lib/phronomy.rb +6 -0
  72. data/scripts/api_snapshot.rb +12 -0
  73. data/scripts/check_readme_ruby.rb +24 -29
  74. data/scripts/check_readme_runnable.rb +92 -81
  75. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  76. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  77. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  78. data/sig/phronomy/agent.rbs +18 -0
  79. data/sig/phronomy/embeddings/base.rbs +6 -0
  80. data/sig/phronomy/execution/execution.rbs +2 -2
  81. data/sig/phronomy/extensions.rbs +1 -0
  82. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  83. data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
  84. data/sig/phronomy/integrations.rbs +14 -0
  85. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  86. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  87. data/sig/phronomy/storage/contracts.rbs +2 -2
  88. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
  89. data/sig/phronomy/workflow.rbs +4 -0
  90. data/sig/phronomy.rbs +4 -0
  91. metadata +49 -2
@@ -9,7 +9,13 @@ module Phronomy
9
9
  @runtime = runtime
10
10
  end
11
11
 
12
- def registry = WorkflowExecutionRegistry.for(@runtime.event_loop)
12
+ def registry
13
+ receiver = WorkflowExecutionRegistry.for(@runtime.event_loop)
14
+ @runtime.__shutdown_participant(key: WorkflowExecutionRegistry) ||
15
+ @runtime.__register_shutdown_participant(key: WorkflowExecutionRegistry, participant: receiver)
16
+ receiver
17
+ end
18
+
13
19
  def existing_registry = WorkflowExecutionRegistry.existing_for(@runtime)
14
20
  def executing? = @runtime.event_loop.current?
15
21
 
@@ -21,18 +27,35 @@ module Phronomy
21
27
  WorkflowPhaseMachineBuilder.new(**definition).build
22
28
  end
23
29
 
24
- def build_session(entry_actions:, persist: nil, **options)
30
+ def build_session(entry_actions:, persist: nil, task_execution: nil, **options)
25
31
  # Preserve nil/scalar results: initial entry must not re-adopt metadata
26
32
  # unless the application actually returns a context.
27
33
  initial_actions = entry_actions.to_h do |state_name, callbacks|
28
34
  [state_name, callbacks.map { |callback| ->(context) { WorkflowActionRules.entry(callback, context, state_name) } }]
29
35
  end
30
- Phronomy::FSMSession.new(**options, entry_actions: initial_actions, event_loop: @runtime.event_loop,
36
+ klass = task_execution ? Phronomy::WorkflowTaskSession : Phronomy::FSMSession
37
+ options = options.merge(task_execution: task_execution) if task_execution
38
+ klass.new(**options, entry_actions: initial_actions, event_loop: @runtime.event_loop,
31
39
  terminal_policy: persist && WorkflowTerminalPolicy.new(persist: persist))
32
40
  end
33
41
 
34
42
  def register_session(session, completion:)
35
43
  @runtime.event_loop.register(session, completion: completion, receiver: registry)
36
44
  end
45
+
46
+ def subscribe_cancellation(token, &callback)
47
+ subscriptions = Phronomy::Concurrency::ResultSubscriptions.new
48
+ subscriptions.cancellation(token, &callback)
49
+ remaining = token.remaining_monotonic_seconds
50
+ if remaining && !token.cancelled?
51
+ timer = @runtime.timer_queue
52
+ timer.schedule(seconds: remaining, &callback)
53
+ subscriptions.add { timer.cancel(callback) }
54
+ end
55
+ subscriptions
56
+ rescue
57
+ subscriptions&.close
58
+ raise
59
+ end
37
60
  end
38
61
  end
@@ -47,7 +47,8 @@ module Phronomy
47
47
  :context,
48
48
  :current_event,
49
49
  :selected_transition_action,
50
- :selected_transition_metadata
50
+ :selected_transition_metadata,
51
+ :workflow_task_execution
51
52
  )
52
53
 
53
54
  state_machine :phase, initial: entry do
@@ -85,6 +86,7 @@ module Phronomy
85
86
  # Both source exit callbacks and the transition action are
86
87
  # before_transition callbacks. Register exits first to preserve the
87
88
  # required exit -> transition action -> entry ordering.
89
+ before_transition { |machine| machine.workflow_task_execution&.leave }
88
90
  exit_actions.each do |state_name, callables|
89
91
  callables.each do |callable|
90
92
  before_transition(
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # I07 translates C02 task-state decisions to generic FSM progression. Engine
5
+ # does not learn TaskResult ownership, cancellation or stale-result policy.
6
+ # @api private
7
+ class WorkflowTaskSession < FSMSession
8
+ def initialize(task_execution:, **options)
9
+ super(**options)
10
+ @task_execution = task_execution
11
+ @task_execution.bind(self)
12
+ end
13
+
14
+ def start
15
+ @task_execution.activate
16
+ super unless @done
17
+ rescue => error
18
+ finish_with_error(error)
19
+ end
20
+
21
+ def apply_task_context(context)
22
+ return unless @terminal_lifecycle_state == :running
23
+
24
+ @ctx = @context = context
25
+ apply_context_metadata!
26
+ @tracker.context = context
27
+ advance_or_halt
28
+ rescue => error
29
+ finish_with_error(error)
30
+ end
31
+
32
+ def fail_task_execution(error)
33
+ finish_with_error(error) if @terminal_lifecycle_state == :running
34
+ end
35
+
36
+ def handle(event)
37
+ return if event.type == :state_completed && !@task_execution.permits_completion?(event.payload)
38
+
39
+ super
40
+ end
41
+
42
+ private
43
+
44
+ def post_session_event(type, payload = nil)
45
+ payload = @task_execution.completion_key if type == :state_completed
46
+ super
47
+ end
48
+
49
+ def advance_or_halt
50
+ if @task_execution.enter(@current_state, @ctx)
51
+ notify_stable_state!
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ def build_tracker(from_state)
58
+ super.tap { |tracker| tracker.workflow_task_execution = @task_execution }
59
+ end
60
+
61
+ def request_terminal!(...)
62
+ @task_execution.close
63
+ super
64
+ end
65
+
66
+ def finish_with_error(error)
67
+ @task_execution.close
68
+ super
69
+ end
70
+
71
+ def retire_without_result!(error)
72
+ @task_execution.retire(error)
73
+ super
74
+ end
75
+ end
76
+ end
data/lib/phronomy.rb CHANGED
@@ -68,6 +68,8 @@ loader.ignore(
68
68
  "#{__dir__}/phronomy/runtime_composition/global_runtime.rb",
69
69
  "#{__dir__}/phronomy/runtime_composition/execution_defaults.rb"
70
70
  )
71
+ # Optional application integrations are loaded explicitly by their consumers.
72
+ loader.ignore("#{__dir__}/phronomy/integrations")
71
73
  # Persistence conformance support must never make production loading require RSpec.
72
74
  loader.ignore(
73
75
  "#{__dir__}/phronomy/testing/persistence_contract.rb",
@@ -85,6 +87,8 @@ loader.ignore("#{__dir__}/phronomy/agent/agent_purged_error.rb")
85
87
  loader.ignore("#{__dir__}/phronomy/agent/stream_callback_error.rb")
86
88
  loader.ignore("#{__dir__}/phronomy/agent/handoff_error.rb")
87
89
  loader.ignore("#{__dir__}/phronomy/agent/agent_already_exists_error.rb")
90
+ loader.ignore("#{__dir__}/phronomy/agent/agent_not_found_error.rb")
91
+ loader.ignore("#{__dir__}/phronomy/agent/agent_unloaded_error.rb")
88
92
  loader.setup
89
93
  require_relative "phronomy/persistence/transaction"
90
94
  require_relative "phronomy/persistence_composition/stores"
@@ -96,6 +100,8 @@ require_relative "phronomy/agent/agent_purged_error"
96
100
  require_relative "phronomy/agent/stream_callback_error"
97
101
  require_relative "phronomy/agent/handoff_error"
98
102
  require_relative "phronomy/agent/agent_already_exists_error"
103
+ require_relative "phronomy/agent/agent_not_found_error"
104
+ require_relative "phronomy/agent/agent_unloaded_error"
99
105
 
100
106
  require_relative "phronomy/runtime_composition/configuration_defaults"
101
107
  require_relative "phronomy/llm_adapter/token_usage"
@@ -18,6 +18,16 @@ require_relative "../lib/phronomy"
18
18
 
19
19
  PUBLIC_API_ENTRIES = [
20
20
  # Stable
21
+ Phronomy::Error,
22
+ Phronomy::AgentNotFoundError,
23
+ Phronomy::AgentUnloadedError,
24
+ Phronomy::Agent,
25
+ Phronomy::Agent::Scope,
26
+ Phronomy::Agent::ScopeCleanupError,
27
+ Phronomy::MultiAgent::TeamNotFoundError,
28
+ Phronomy::MultiAgent::TeamBusyError,
29
+ Phronomy::Embeddings::AuthenticationError,
30
+ Phronomy::Embeddings::RateLimitError,
21
31
  Phronomy::Agent::Base,
22
32
  Phronomy::Agent::ReservedExecution,
23
33
  Phronomy::Agent::ExecutionObservation,
@@ -36,6 +46,8 @@ PUBLIC_API_ENTRIES = [
36
46
  Phronomy::InvocationContext,
37
47
  Phronomy::Tool::Base,
38
48
  Phronomy::Workflow,
49
+ Phronomy::Workflow::Builder,
50
+ Phronomy::WorkflowTaskResumeError,
39
51
  Phronomy::WorkflowContext,
40
52
  Phronomy::Runnable,
41
53
  Phronomy::Context::PromptTemplate,
@@ -1,38 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Extracts every ```ruby ... ``` block from README.md and runs `ruby -c` on each.
4
- # Exits non-zero if any block has a syntax error.
5
-
3
+ # Check all Ruby fences, including executable examples, in the entry documents.
6
4
  require "tempfile"
7
5
  require "open3"
8
-
9
- readme_path = File.expand_path("../README.md", __dir__)
10
- readme = File.read(readme_path)
11
- blocks = readme.scan(/^```ruby\n(.*?)^```/m).map.with_index(1) { |(code), i| [i, code] }
12
-
13
- puts "Checking #{blocks.size} Ruby code blocks in README.md..."
6
+ require_relative "check_readme_runnable"
14
7
 
15
8
  failures = []
16
-
17
- blocks.each do |index, code|
18
- Tempfile.create(["readme_block_#{index}", ".rb"]) do |f|
19
- f.write(code)
20
- f.flush
21
- stdout, status = Open3.capture2e("ruby", "-c", f.path)
22
- if status.success?
23
- puts " OK block ##{index}"
24
- else
25
- failures << index
26
- puts " FAIL block ##{index}"
27
- puts stdout.gsub(f.path, "block ##{index}")
9
+ count = 0
10
+ paths = ARGV.empty? ? RunnableDocumentation::PATHS : ARGV
11
+ paths.each do |relative|
12
+ text = File.read(File.expand_path(relative, RunnableDocumentation::ROOT))
13
+ text.to_enum(:scan, /^```ruby(?: runnable)?\n(.*?)^```/m).each do
14
+ match = Regexp.last_match
15
+ line = text[0...match.begin(1)].count("\n") + 1
16
+ label = "#{relative}:#{line}"
17
+ count += 1
18
+ Tempfile.create(["documentation_block", ".rb"]) do |file|
19
+ file.write(match[1])
20
+ file.flush
21
+ out, status = Open3.capture2e(RbConfig.ruby, "-c", file.path)
22
+ if status.success?
23
+ puts "OK #{label}"
24
+ else
25
+ failures << label
26
+ puts "FAIL #{label}"
27
+ puts out.gsub(file.path, label)
28
+ end
28
29
  end
29
30
  end
30
31
  end
31
-
32
- if failures.empty?
33
- puts "All #{blocks.size} Ruby code blocks passed syntax check."
34
- exit 0
35
- else
36
- puts "\n#{failures.size} block(s) failed syntax check: #{failures.join(", ")}"
37
- exit 1
38
- end
32
+ puts "#{count} Ruby blocks; #{failures.size} syntax failures"
33
+ exit(failures.empty? ? 0 : 1)
@@ -1,101 +1,112 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # scripts/check_readme_runnable.rb
4
- #
5
- # Extracts ```ruby runnable blocks from README.md and executes each in an
6
- # isolated subprocess with a fake LLM stub to catch API drift.
7
- #
8
- # Any block that raises NoMethodError / ArgumentError / NameError causes a
9
- # non-zero exit, failing the CI step.
10
- #
11
- # Usage (from the phronomy/ root):
12
- # bundle exec ruby scripts/check_readme_runnable.rb
13
-
3
+ # Runs annotated, self-contained examples through the real public API.
4
+ # Only the external LLM adapter is replaced. Each block has its own process,
5
+ # configuration and Runtime; an accidental network request fails immediately.
6
+ # Usage: bundle exec ruby scripts/check_readme_runnable.rb [markdown paths...]
14
7
  require "tempfile"
15
8
  require "open3"
16
9
 
17
- REPO_ROOT = File.expand_path("..", __dir__)
18
- README_PATH = File.join(REPO_ROOT, "README.md")
19
-
20
- # Injected before every runnable block.
21
- # Uses the Gemfile of this project so subprocesses can load phronomy.
22
- PREAMBLE = <<~RUBY
23
- # frozen_string_literal: true
24
- # --- CI preamble: stub LLM calls so no real network requests are made ---
25
- ENV["BUNDLE_GEMFILE"] ||= "#{File.join(REPO_ROOT, "Gemfile")}"
26
- require "bundler/setup"
27
- require "phronomy"
10
+ module RunnableDocumentation
11
+ ROOT = File.expand_path("..", __dir__)
12
+ PATHS = %w[README.md docs/getting-started.md docs/application-recipes.md docs/workflow-completion.md].freeze
13
+
14
+ PREAMBLE = <<~RUBY
15
+ require "bundler/setup"
16
+ require "phronomy"
17
+ require "webmock"
18
+ require "timeout"
19
+ WebMock.enable!
20
+ WebMock.disable_net_connect!
21
+
22
+ class DocumentationLLM < Phronomy::LLMAdapter::Base
23
+ protected
24
+
25
+ def perform_complete(request, cancellation_token:)
26
+ previous = request.messages.last
27
+ if previous&.role == :tool
28
+ return Phronomy::LLMAdapter::Response.new(content: previous.content)
29
+ end
28
30
 
29
- # Patch invoke methods to return canned responses instead of calling the LLM.
30
- module Phronomy
31
- module Agent
32
- # Prepend overrides AsyncEventApi (which is also prepended) so invoke
33
- # returns a canned response without triggering the EventLoop.
34
- module CiInvokeStub
35
- def invoke(input = nil, **)
36
- {output: "ci-stub-output", messages: []}
31
+ search = request.tools.find do |tool|
32
+ tool.fetch("parameters_schema").fetch("properties", {}).key?("query")
37
33
  end
34
+ if search
35
+ return Phronomy::LLMAdapter::Response.new(tool_calls: [
36
+ Phronomy::Tool::CallRequest.new(
37
+ id: SecureRandom.uuid, name: search.fetch("name"),
38
+ arguments: {"query" => "Ruby AI frameworks"}
39
+ )
40
+ ])
41
+ end
42
+
43
+ Phronomy::LLMAdapter::Response.new(content: "ci-stub-output")
38
44
  end
39
- Base.prepend(CiInvokeStub)
40
45
 
41
- class Runner
42
- def invoke(input = nil, **)
43
- {output: "ci-stub-output", agent: nil, messages: []}
44
- end
46
+ def perform_stream(request, cancellation_token:)
47
+ response = perform_complete(request, cancellation_token: cancellation_token)
48
+ yield Phronomy::LLMAdapter::StreamChunk.new(content: response.content) if response.content
49
+ response
45
50
  end
46
51
  end
52
+ RUBY
47
53
 
48
- module Chain
49
- class LLMChain
50
- def invoke(vars = {})
51
- "ci-stub-chain"
52
- end
53
- end
54
+ def self.blocks(path)
55
+ text = File.read(path)
56
+ text.to_enum(:scan, /^```ruby runnable\n(.*?)^```/m).map do
57
+ match = Regexp.last_match
58
+ [text[0...match.begin(1)].count("\n") + 1, match[1]]
54
59
  end
55
60
  end
56
- # --- end CI preamble ---
57
-
58
- RUBY
59
-
60
- readme = File.read(README_PATH)
61
61
 
62
- # Match opening fence with 'runnable' annotation: ```ruby runnable
63
- blocks = readme.scan(/^```ruby runnable\n(.*?)^```/m).map.with_index(1) { |(code), i| [i, code] }
64
-
65
- if blocks.empty?
66
- puts "No 'ruby runnable' blocks found in README.md."
67
- exit 0
68
- end
69
-
70
- puts "Checking #{blocks.size} runnable Ruby block(s) in README.md..."
71
-
72
- failures = []
73
-
74
- blocks.each do |index, code|
75
- Tempfile.create(["readme_runnable_#{index}", ".rb"]) do |f|
76
- f.write(PREAMBLE)
77
- f.write(code)
78
- f.flush
79
-
80
- out, err, status = Open3.capture3(RbConfig.ruby, f.path)
81
- combined = (out + err).gsub(f.path, "block ##{index}")
82
-
83
- if status.success?
84
- puts " OK block ##{index}"
85
- else
86
- failures << index
87
- puts " FAIL block ##{index}"
88
- # Print at most 15 lines of output to keep CI logs readable.
89
- puts combined.lines.first(15).join
62
+ def self.check(paths = PATHS, output: $stdout)
63
+ failures = []
64
+ count = 0
65
+ paths.each do |relative|
66
+ path = File.expand_path(relative, ROOT)
67
+ examples = blocks(path)
68
+ if examples.empty?
69
+ failures << relative
70
+ output.puts "FAIL #{relative}: no ruby runnable blocks"
71
+ end
72
+ examples.each do |line, code|
73
+ count += 1
74
+ label = "#{relative}:#{line}"
75
+ Tempfile.create(["phronomy_documentation", ".rb"]) do |file|
76
+ file.write(PREAMBLE)
77
+ file.write(<<~RUBY)
78
+ Phronomy.with_configuration do |configuration|
79
+ configuration.llm_adapter = DocumentationLLM.new
80
+ begin
81
+ Timeout.timeout(20) do
82
+ eval(#{code.dump}, TOPLEVEL_BINDING, #{path.dump}, #{line})
83
+ end
84
+ ensure
85
+ Phronomy::Runtime.reset_default!(timeout: 5)
86
+ end
87
+ end
88
+ RUBY
89
+ file.flush
90
+ out, err, status = Open3.capture3(
91
+ {"BUNDLE_GEMFILE" => File.join(ROOT, "Gemfile")},
92
+ RbConfig.ruby, file.path, chdir: ROOT
93
+ )
94
+ if status.success?
95
+ output.puts "OK #{label}"
96
+ else
97
+ failures << label
98
+ output.puts "FAIL #{label}"
99
+ output.puts (out + err).gsub(file.path, label).lines.first(15).join
100
+ end
101
+ end
102
+ end
90
103
  end
104
+ output.puts "#{count} runnable examples; #{failures.size} failures"
105
+ failures.empty?
91
106
  end
92
107
  end
93
108
 
94
- puts
95
- if failures.empty?
96
- puts "All #{blocks.size} runnable block(s) passed."
97
- exit 0
98
- else
99
- puts "#{failures.size} block(s) failed: #{failures.join(", ")}"
100
- exit 1
109
+ if $PROGRAM_NAME == __FILE__
110
+ paths = ARGV.empty? ? RunnableDocumentation::PATHS : ARGV
111
+ exit(RunnableDocumentation.check(paths) ? 0 : 1)
101
112
  end
@@ -0,0 +1,6 @@
1
+ module Phronomy
2
+ class AgentNotFoundError < Phronomy::Persistence::NotFoundError
3
+ attr_reader agent_id: String
4
+ def initialize: (agent_id: untyped) -> void
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Phronomy
2
+ class AgentUnloadedError < Phronomy::Error
3
+ attr_reader agent_id: String
4
+ def initialize: (agent_id: String) -> void
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Phronomy
2
+ class StreamCallbackError < Error
3
+ attr_reader event_type: Symbol
4
+ attr_reader original_error: Exception
5
+ attr_reader result: untyped
6
+ def initialize: (event_type: Symbol, original_error: Exception, ?result: untyped, ?code: String?) -> void
7
+ end
8
+ end
@@ -1,5 +1,21 @@
1
1
  module Phronomy
2
2
  module Agent
3
+ # The block's TaskResult is awaited and unwrapped; Scope itself is process-local.
4
+ def self.open_scope: [R] () { (Scope) -> (R | Phronomy::TaskResult[R]) } -> R
5
+
6
+ class Scope
7
+ def create: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Base
8
+ def create_async: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
9
+ def load: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Base
10
+ def load_async: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
11
+ end
12
+
13
+ class ScopeCleanupError < Phronomy::Error
14
+ attr_reader failures: Array[{ agent_id: String, error: Exception }]
15
+ attr_reader result: untyped
16
+ def initialize: (failures: Array[{ agent_id: String, error: Exception }], result: untyped) -> void
17
+ end
18
+
3
19
  interface _BeforeLLMInputHook
4
20
  def call: (Phronomy::Context::LLMInputBuildContext context) -> Phronomy::Context::LLMInputPatch?
5
21
  end
@@ -108,6 +124,8 @@ module Phronomy
108
124
  def reset_context!: () -> untyped
109
125
  def close!: () -> untyped
110
126
  def purge!: () -> bool
127
+ def unload_async: () -> Phronomy::TaskResult[bool]
128
+ def unload!: (?timeout: Numeric?) -> bool
111
129
  end
112
130
  end
113
131
  end
@@ -6,6 +6,12 @@ module Phronomy
6
6
  class TransportError < Phronomy::Error
7
7
  end
8
8
 
9
+ class AuthenticationError < TransportError
10
+ end
11
+
12
+ class RateLimitError < TransportError
13
+ end
14
+
9
15
  class Base
10
16
  def embed: (String text, ?_CancellationSignal? cancellation_token) -> Array[Float]
11
17
 
@@ -30,12 +30,12 @@ module Phronomy
30
30
 
31
31
  class ExecutionTimeoutError < TimeoutError
32
32
  attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
33
- def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
33
+ def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]], ?code: String?) -> void
34
34
  end
35
35
 
36
36
  class ExecutionCancellationError < CancellationError
37
37
  attr_reader outcomes: Array[TaskResult::Outcome[untyped]]
38
- def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]]) -> void
38
+ def initialize: (?String message, outcomes: Array[TaskResult::Outcome[untyped]], ?code: String?) -> void
39
39
  end
40
40
 
41
41
  end
@@ -14,6 +14,7 @@ module Phronomy
14
14
 
15
15
  module Tracing
16
16
  class Base
17
+ def self.trace_with: [A] (untyped tracer, String name, ?record_output: bool, ?attributes: Hash[Symbol, untyped]) { (untyped span) -> [A, untyped] } -> A
17
18
  def start_span: (String name, **untyped attributes) -> untyped
18
19
  def finish_span: (untyped span, ?output: untyped, ?usage: untyped, ?error: Exception?) -> untyped
19
20
  def trace: [A] (String name, ?input: untyped, **untyped meta) { (untyped span) -> [A, untyped] } -> A
@@ -0,0 +1,6 @@
1
+ module Phronomy
2
+ class FilterBlockError < Error
3
+ attr_reader filter: untyped
4
+ def initialize: (String message, ?filter: untyped, ?code: String?) -> void
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Phronomy
2
+ class LowConfidenceError < Error
3
+ attr_reader result: untyped
4
+ def initialize: (untyped result, ?code: String?) -> void
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ module Phronomy
2
+ module Integrations
3
+ class OrderedEventDelivery
4
+ class ClosedError < Phronomy::Error
5
+ end
6
+ class OverflowError < Phronomy::Error
7
+ end
8
+ def self.open: [T] (?capacity: Integer, ?batch_size: Integer, ?flush_timeout: Numeric?, deliver: ^(untyped) -> untyped, ?prepare_batch: (^(Array[untyped]) -> Array[untyped])?) { (OrderedEventDelivery) -> T } -> T
9
+ def initialize: (?capacity: Integer, ?batch_size: Integer, ?prepare_batch: (^(Array[untyped]) -> Array[untyped])?) { (untyped) -> untyped } -> void
10
+ def publish: (untyped) -> self
11
+ def close_and_wait: (?timeout: Numeric?) -> self
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module Phronomy
2
+ module MultiAgent
3
+ class TeamBusyError < Phronomy::Error
4
+ attr_reader team_id: String
5
+ def initialize: (?String? message, team_id: untyped) -> void
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Phronomy
2
+ module MultiAgent
3
+ class TeamNotFoundError < Phronomy::Persistence::NotFoundError
4
+ attr_reader team_id: String
5
+ def initialize: (?String? message, team_id: untyped) -> void
6
+ end
7
+ end
8
+ end
@@ -16,11 +16,11 @@ module Phronomy
16
16
  class UniqueConstraintError < ConflictError
17
17
  attr_reader resource: String
18
18
  attr_reader constraint: Symbol
19
- def initialize: (resource: Resource, constraint: Symbol) -> void
19
+ def initialize: (resource: Resource, constraint: Symbol, ?code: String?) -> void
20
20
  end
21
21
  class ConditionFailedError < ConflictError
22
22
  attr_reader condition: condition
23
- def initialize: (condition) -> void
23
+ def initialize: (condition, ?code: String?) -> void
24
24
  end
25
25
  class BlobConflictError < ConflictError
26
26
  end
@@ -10,5 +10,6 @@ module Phronomy
10
10
  def compile_transitions: (**untyped definition) -> untyped
11
11
  def build_session: (**untyped options) -> untyped
12
12
  def register_session: (untyped session, completion: TaskResult[untyped]) -> untyped
13
+ def subscribe_cancellation: (Concurrency::CancellationToken token) { () -> void } -> untyped
13
14
  end
14
15
  end
@@ -1,4 +1,7 @@
1
1
  module Phronomy
2
+ class WorkflowTaskResumeError < Error
3
+ def initialize: (?String message, ?code: String?) -> void
4
+ end
2
5
  module WorkflowContext
3
6
  def phase: () -> Symbol
4
7
  def halted?: () -> bool
@@ -20,6 +23,7 @@ module Phronomy
20
23
  class Builder
21
24
  def initial: (Symbol state_name) -> untyped
22
25
  def state: (Symbol name, ?action: untyped) -> untyped
26
+ def task_state: (Symbol name, start: ^(untyped) -> TaskResult[untyped], apply: ^(untyped, untyped) -> untyped) -> void
23
27
  def entry: (Symbol name, untyped callable) -> untyped
24
28
  def exit: (Symbol name, untyped callable) -> untyped
25
29
  def wait_state: (Symbol name) -> untyped
data/sig/phronomy.rbs CHANGED
@@ -2,6 +2,10 @@ module Phronomy
2
2
  VERSION: String
3
3
 
4
4
  class Error < StandardError
5
+ attr_reader code: String?
6
+ def initialize: (?untyped message, ?code: String?) -> void
7
+ def self.diagnostic: (Exception) -> Hash[String, untyped]
8
+ def self.from_diagnostic: (Hash[untyped, untyped], ?remaining: Integer) -> Error
5
9
  end
6
10
 
7
11
  class ParseError < Error