phronomy 0.28.0 → 0.30.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +32 -6
  4. data/docs/application-recipes.md +229 -0
  5. data/docs/architecture/agent-configuration-and-tool-binding.md +8 -0
  6. data/docs/architecture/agent-iteration-budget.md +81 -0
  7. data/docs/architecture/dx05-agent-scope.md +126 -0
  8. data/docs/architecture/dx05-workflow-task-state.md +134 -0
  9. data/docs/architecture/error-api-review.md +245 -0
  10. data/docs/architecture/generator-verifier-lifecycle-result.md +126 -0
  11. data/docs/architecture/generator-verifier-ownership.md +18 -109
  12. data/docs/architecture/llm-continuation-metadata.md +88 -0
  13. data/docs/architecture/lm-studio-output-failures.md +111 -0
  14. data/docs/architecture/r8-unit15.md +3 -0
  15. data/docs/architecture/r8-unit7.md +2 -1
  16. data/docs/architecture/structured-output-and-scope.md +183 -0
  17. data/docs/architecture/tracing.md +22 -4
  18. data/docs/architecture/transport-error-boundaries.md +103 -0
  19. data/docs/architecture/trust-acceptance-followup.md +87 -0
  20. data/docs/failure-reason-codes.md +45 -0
  21. data/docs/features.md +14 -2
  22. data/docs/getting-started.md +44 -43
  23. data/docs/migrations/r8-unit15.md +3 -0
  24. data/docs/ordered-event-delivery.md +54 -0
  25. data/docs/workflow-completion.md +92 -0
  26. data/gemfiles/http_minimum.gemfile +10 -0
  27. data/lib/phronomy/agent/admission.rb +1 -1
  28. data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
  29. data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
  30. data/lib/phronomy/agent/api/agent.rb +13 -0
  31. data/lib/phronomy/agent/async_event_api.rb +121 -88
  32. data/lib/phronomy/agent/base.rb +82 -44
  33. data/lib/phronomy/agent/before_llm_input.rb +7 -1
  34. data/lib/phronomy/agent/context_importer.rb +4 -0
  35. data/lib/phronomy/agent/execution/agent_invocation.rb +6 -2
  36. data/lib/phronomy/agent/execution/dispatch_preparation.rb +6 -1
  37. data/lib/phronomy/agent/execution/exact_execution.rb +20 -9
  38. data/lib/phronomy/agent/execution/execution_cancellation.rb +1 -1
  39. data/lib/phronomy/agent/execution/execution_coordinator.rb +79 -18
  40. data/lib/phronomy/agent/execution/execution_outcome_committer.rb +24 -4
  41. data/lib/phronomy/agent/execution/execution_registry.rb +5 -1
  42. data/lib/phronomy/agent/execution/initial_preparation.rb +11 -5
  43. data/lib/phronomy/agent/execution/invocation_actions.rb +1 -1
  44. data/lib/phronomy/agent/execution/invocation_transitions.rb +4 -4
  45. data/lib/phronomy/agent/execution/iteration_budget.rb +35 -0
  46. data/lib/phronomy/agent/execution/runtime_record_encoder.rb +4 -4
  47. data/lib/phronomy/agent/execution_observation.rb +5 -5
  48. data/lib/phronomy/agent/filterable.rb +21 -15
  49. data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
  50. data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
  51. data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
  52. data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
  53. data/lib/phronomy/agent/lifecycle/scope.rb +208 -0
  54. data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +21 -0
  55. data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
  56. data/lib/phronomy/agent/output_contract.rb +64 -0
  57. data/lib/phronomy/agent/output_error.rb +53 -0
  58. data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +6 -6
  59. data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +8 -7
  60. data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
  61. data/lib/phronomy/agent/recovery/recovery_coordinator.rb +6 -10
  62. data/lib/phronomy/agent/recovery/recovery_support.rb +6 -8
  63. data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +2 -1
  64. data/lib/phronomy/agent/runtime_input.rb +15 -4
  65. data/lib/phronomy/agent/saved_context_reader.rb +2 -1
  66. data/lib/phronomy/agent/store.rb +10 -2
  67. data/lib/phronomy/agent/stream_callback_error.rb +2 -2
  68. data/lib/phronomy/common/error.rb +65 -1
  69. data/lib/phronomy/common/values/immutable.rb +33 -0
  70. data/lib/phronomy/content_store/base.rb +7 -2
  71. data/lib/phronomy/context/assembly.rb +18 -22
  72. data/lib/phronomy/embeddings/authentication_error.rb +11 -0
  73. data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +8 -2
  74. data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
  75. data/lib/phronomy/engine/fsm_session.rb +16 -1
  76. data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
  77. data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
  78. data/lib/phronomy/execution/task_result.rb +7 -2
  79. data/lib/phronomy/filter/filter_block_error.rb +2 -2
  80. data/lib/phronomy/generation/generator_verifier/agent_operation.rb +28 -5
  81. data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +14 -27
  82. data/lib/phronomy/generation/generator_verifier/approval.rb +30 -0
  83. data/lib/phronomy/generation/generator_verifier/cleanup_error.rb +15 -0
  84. data/lib/phronomy/generation/generator_verifier/invocation.rb +147 -0
  85. data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +16 -0
  86. data/lib/phronomy/generation/generator_verifier/protocol.rb +55 -0
  87. data/lib/phronomy/generation/generator_verifier/result.rb +121 -0
  88. data/lib/phronomy/generation/generator_verifier/unaccepted_result_error.rb +14 -0
  89. data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +13 -11
  90. data/lib/phronomy/generation/generator_verifier.rb +170 -85
  91. data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
  92. data/lib/phronomy/llm_adapter/backends/lm_studio.rb +139 -0
  93. data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +134 -12
  94. data/lib/phronomy/llm_adapter/backends/structured_response_capture.rb +41 -0
  95. data/lib/phronomy/llm_adapter/message.rb +4 -0
  96. data/lib/phronomy/llm_adapter/request.rb +17 -2
  97. data/lib/phronomy/llm_adapter/response.rb +11 -4
  98. data/lib/phronomy/llm_adapter/structured_output_unsupported_error.rb +14 -0
  99. data/lib/phronomy/llm_adapter/transport_error.rb +4 -0
  100. data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
  101. data/lib/phronomy/multi_agent/shared_state.rb +1 -0
  102. data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
  103. data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
  104. data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
  105. data/lib/phronomy/output_parser/json_schema_parser.rb +113 -0
  106. data/lib/phronomy/output_parser/parse_error.rb +10 -1
  107. data/lib/phronomy/runtime_composition/agent_defaults.rb +4 -0
  108. data/lib/phronomy/storage/condition_failed_error.rb +2 -2
  109. data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
  110. data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
  111. data/lib/phronomy/tool/base.rb +25 -23
  112. data/lib/phronomy/tracing/automatic.rb +14 -8
  113. data/lib/phronomy/tracing/base.rb +37 -7
  114. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -3
  115. data/lib/phronomy/tracing/observation.rb +2 -1
  116. data/lib/phronomy/tracing/open_telemetry_tracer.rb +7 -32
  117. data/lib/phronomy/version.rb +1 -1
  118. data/lib/phronomy/workflow/execution/workflow.rb +30 -2
  119. data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
  120. data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
  121. data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
  122. data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
  123. data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
  124. data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
  125. data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
  126. data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
  127. data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
  128. data/lib/phronomy.rb +7 -2
  129. data/scripts/api_snapshot.rb +15 -0
  130. data/scripts/check_readme_ruby.rb +24 -29
  131. data/scripts/check_readme_runnable.rb +92 -81
  132. data/scripts/migrate_generator_verifier_result.rb +18 -0
  133. data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
  134. data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
  135. data/sig/phronomy/agent/execution_contract.rbs +4 -3
  136. data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
  137. data/sig/phronomy/agent.rbs +36 -5
  138. data/sig/phronomy/embeddings/base.rbs +6 -0
  139. data/sig/phronomy/execution/execution.rbs +2 -2
  140. data/sig/phronomy/extensions.rbs +8 -2
  141. data/sig/phronomy/filter/filter_block_error.rbs +6 -0
  142. data/sig/phronomy/generation/generator_verifier.rbs +70 -0
  143. data/sig/phronomy/integrations.rbs +14 -0
  144. data/sig/phronomy/llm_adapter/backends/lm_studio.rbs +8 -0
  145. data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +1 -0
  146. data/sig/phronomy/llm_adapter/values.rbs +8 -2
  147. data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
  148. data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
  149. data/sig/phronomy/storage/contracts.rbs +2 -2
  150. data/sig/phronomy/tracing/observation.rbs +1 -1
  151. data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
  152. data/sig/phronomy/workflow.rbs +4 -0
  153. data/sig/phronomy.rbs +6 -0
  154. metadata +103 -5
  155. data/lib/phronomy/generation/generator_verifier/default_parser.rb +0 -27
  156. data/lib/phronomy/generation/low_confidence_error.rb +0 -14
  157. data/lib/phronomy/runtime_composition/generation_defaults.rb +0 -6
@@ -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
@@ -12,6 +12,7 @@ loader.inflector.inflect("fsm_protocol" => "FSMProtocol")
12
12
  loader.inflector.inflect("llm_adapter" => "LLMAdapter")
13
13
  loader.inflector.inflect("llm_operation_result" => "LLMOperationResult")
14
14
  loader.inflector.inflect("ruby_llm" => "RubyLLM")
15
+ loader.inflector.inflect("lm_studio" => "LMStudio")
15
16
  loader.inflector.inflect("canonical_json" => "CanonicalJSON")
16
17
  loader.inflector.inflect("llm_call_record" => "LLMCallRecord")
17
18
  loader.inflector.inflect("llm_input_manifest" => "LLMInputManifest")
@@ -60,7 +61,6 @@ loader.ignore(
60
61
  "#{__dir__}/phronomy/runtime_composition/global_configuration.rb",
61
62
  "#{__dir__}/phronomy/agent/composition",
62
63
  "#{__dir__}/phronomy/runtime_composition/agent_defaults.rb",
63
- "#{__dir__}/phronomy/runtime_composition/generation_defaults.rb",
64
64
  "#{__dir__}/phronomy/runtime_composition/evaluation_defaults.rb",
65
65
  "#{__dir__}/phronomy/runtime_composition/workflow_defaults.rb",
66
66
  "#{__dir__}/phronomy/runtime_composition/multi_agent_defaults.rb",
@@ -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"
@@ -110,7 +116,6 @@ require_relative "phronomy/agent/api/agent"
110
116
  require_relative "phronomy/runtime_composition/agent_defaults"
111
117
  require_relative "phronomy/runtime_composition/multi_agent_defaults"
112
118
  require_relative "phronomy/runtime_composition/workflow_defaults"
113
- require_relative "phronomy/runtime_composition/generation_defaults"
114
119
  require_relative "phronomy/runtime_composition/evaluation_defaults"
115
120
  require_relative "phronomy/agent/composition/run_once"
116
121
 
@@ -18,6 +18,17 @@ 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::OutputError,
27
+ Phronomy::Agent::ScopeCleanupError,
28
+ Phronomy::MultiAgent::TeamNotFoundError,
29
+ Phronomy::MultiAgent::TeamBusyError,
30
+ Phronomy::Embeddings::AuthenticationError,
31
+ Phronomy::Embeddings::RateLimitError,
21
32
  Phronomy::Agent::Base,
22
33
  Phronomy::Agent::ReservedExecution,
23
34
  Phronomy::Agent::ExecutionObservation,
@@ -36,6 +47,8 @@ PUBLIC_API_ENTRIES = [
36
47
  Phronomy::InvocationContext,
37
48
  Phronomy::Tool::Base,
38
49
  Phronomy::Workflow,
50
+ Phronomy::Workflow::Builder,
51
+ Phronomy::WorkflowTaskResumeError,
39
52
  Phronomy::WorkflowContext,
40
53
  Phronomy::Runnable,
41
54
  Phronomy::Context::PromptTemplate,
@@ -45,6 +58,8 @@ PUBLIC_API_ENTRIES = [
45
58
  Phronomy::Tool::Operation,
46
59
  Phronomy::LLMAdapter::Base,
47
60
  Phronomy::LLMAdapter::Request,
61
+ Phronomy::LLMAdapter::StructuredOutputUnsupportedError,
62
+ Phronomy::OutputParser::JsonSchemaParser,
48
63
  Phronomy::LLMAdapter::Message,
49
64
  Phronomy::LLMAdapter::Response,
50
65
  Phronomy::LLMAdapter::StreamChunk,
@@ -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,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Explicit one-time conversion of a pre-structured-output Result JSON object.
5
+ # Never infer old formats in the runtime reader; never overwrite the source.
6
+ require_relative "../lib/phronomy"
7
+
8
+ abort "Usage: ruby scripts/migrate_generator_verifier_result.rb OLD.json NEW.json" unless ARGV.length == 2
9
+ source, destination = ARGV
10
+ value = JSON.parse(File.read(source), allow_duplicate_key: false, allow_nan: false,
11
+ allow_comments: false, allow_invalid_escape: false, allow_control_characters: false)
12
+ abort "Expected an old Result object without failure" unless value.is_a?(Hash) && !value.key?("failure")
13
+ value["failure"] = nil
14
+ result = Phronomy::GeneratorVerifier::Result.from_h(value)
15
+ File.open(destination, File::WRONLY | File::CREAT | File::EXCL) do |file|
16
+ file.write(JSON.pretty_generate(result.to_h))
17
+ file.write("\n")
18
+ 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
@@ -2,7 +2,7 @@ module Phronomy
2
2
 
3
3
  module Agent
4
4
  type execution_wiring = { ?phronomy_execution_participant: _ExecutionParticipant, ?phronomy_control_bindings: Array[_ControlCapability], ?phronomy_transfer_context: TransferContext, ?phronomy_admission: _AdmissionOwner, ?phronomy_reservation: Hash[String, json_value] }
5
- type execution_result = { agent_id: String, execution_id: String, status: Symbol, result: String?, output: String?, error: Hash[String, json_value]? }
5
+ type execution_result = { agent_id: String, execution_id: String, status: Symbol, result: json_value, output: json_value, ?raw_output: String?, error: Hash[String, json_value]? }
6
6
 
7
7
  class ReservedExecution < Data
8
8
  attr_reader agent_id: String
@@ -15,12 +15,13 @@ module Phronomy
15
15
  attr_reader agent_id: String
16
16
  attr_reader execution_id: String
17
17
  attr_reader status: Symbol
18
- attr_reader result: String?
18
+ attr_reader result: json_value
19
+ attr_reader raw_output: String?
19
20
  attr_reader error: Hash[String, json_value]?
20
21
  attr_reader reservation: ReservedExecution?
21
22
  attr_reader transfer_receipt: Hash[String, json_value]?
22
23
  attr_reader cancellation_requested: bool
23
- def self.new: (agent_id: String, execution_id: String, status: Symbol, result: String?, error: Hash[String, json_value]?, reservation: ReservedExecution?, transfer_receipt: Hash[String, json_value]?, cancellation_requested: bool) -> instance
24
+ def self.new: (agent_id: String, execution_id: String, status: Symbol, result: json_value, ?raw_output: String?, error: Hash[String, json_value]?, reservation: ReservedExecution?, transfer_receipt: Hash[String, json_value]?, cancellation_requested: bool) -> instance
24
25
  def self.from_result: (execution_result) -> instance
25
26
  def absent?: () -> bool
26
27
  def terminal?: () -> bool
@@ -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,34 @@
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 release!: () -> bool
8
+ def create: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Base
9
+ def create_async: (singleton(Base) agent_class, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
10
+ def load: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Base
11
+ def load_async: (singleton(Base) agent_class, String agent_id, persistence: Store, **untyped options) ?{ (untyped event) -> void } -> Phronomy::TaskResult[Base]
12
+ end
13
+
14
+ class ScopeCleanupError < Phronomy::Error
15
+ attr_reader failures: Array[{ agent_id: String?, error: Exception }]
16
+ attr_reader result: untyped
17
+ attr_reader scope: Scope
18
+ attr_reader primary_error: Exception?
19
+ def initialize: (scope: Scope, failures: Array[{ agent_id: String?, error: Exception }], ?result: untyped, ?primary_error: Exception?) -> void
20
+ end
21
+
22
+ class OutputError < Phronomy::Error
23
+ attr_reader execution_id: String
24
+ attr_reader schema_digest: String
25
+ attr_reader reason: String
26
+ attr_reader violations: Array[Hash[String, untyped]]
27
+ attr_reader raw_output: untyped
28
+ def initialize: (execution_id: String, schema_digest: String, reason: String | Symbol,
29
+ ?violations: Array[Hash[String, untyped]], ?raw_output: untyped) -> void
30
+ end
31
+
3
32
  interface _BeforeLLMInputHook
4
33
  def call: (Phronomy::Context::LLMInputBuildContext context) -> Phronomy::Context::LLMInputPatch?
5
34
  end
@@ -79,7 +108,7 @@ module Phronomy
79
108
  attr_reader persistence: Store
80
109
  def knowledge_snapshot: () -> Array[KnowledgeItem]
81
110
  def event_listener: () -> (^(Object) -> void)?
82
- def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
111
+ def start_reserved_async: (reservation: ReservedExecution, input: String, ?config: Hash[Symbol, Object], ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[execution_result]
83
112
  def resume_async: (String execution_id, ?config: Hash[Symbol, Object]) -> TaskResult[execution_result]
84
113
  def resume: (String execution_id, ?config: Hash[Symbol, Object]) -> execution_result
85
114
  def cancel_async: (String execution_id) -> TaskResult[ExecutionObservation]
@@ -87,10 +116,10 @@ module Phronomy
87
116
 
88
117
  # Public operation methods no longer accept per-invocation event listeners
89
118
  # or event-listener blocks.
90
- def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
91
- def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
92
- def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> Hash[Symbol, untyped]
93
- def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped) -> TaskResult[Hash[Symbol, untyped]]
119
+ def invoke: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
120
+ def invoke_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
121
+ def stream: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> Hash[Symbol, untyped]
122
+ def stream_async: (untyped input, ?config: Hash[untyped, untyped], ?invocation_context: untyped, ?output_schema: Hash[String | Symbol, untyped]?) -> TaskResult[Hash[Symbol, untyped]]
94
123
 
95
124
  def approve: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> untyped
96
125
  def approve_async: (String execution_id, approval_request_id: String, ?approved: bool, ?config: Hash[untyped, untyped]) -> TaskResult[untyped]
@@ -108,6 +137,8 @@ module Phronomy
108
137
  def reset_context!: () -> untyped
109
138
  def close!: () -> untyped
110
139
  def purge!: () -> bool
140
+ def unload_async: () -> Phronomy::TaskResult[bool]
141
+ def unload!: (?timeout: Numeric?) -> bool
111
142
  end
112
143
  end
113
144
  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