phronomy 0.15.1 → 0.17.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -9
  3. data/CHANGELOG.md +159 -28
  4. data/CONTRIBUTING.md +28 -16
  5. data/README.md +400 -143
  6. data/benchmark/baseline.json +2 -3
  7. data/benchmark/bench_agent_invoke.rb +7 -4
  8. data/benchmark/bench_context_assembler.rb +134 -34
  9. data/benchmark/bench_regression.rb +3 -19
  10. data/benchmark/bench_tool_schema.rb +2 -34
  11. data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
  12. data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
  13. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
  14. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
  15. data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
  16. data/lib/phronomy/agent/activation_registry.rb +28 -0
  17. data/lib/phronomy/agent/agent_execution.rb +97 -0
  18. data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
  19. data/lib/phronomy/agent/agent_invocation.rb +44 -46
  20. data/lib/phronomy/agent/agent_invocation_session_builder.rb +206 -104
  21. data/lib/phronomy/agent/agent_root.rb +66 -0
  22. data/lib/phronomy/agent/async_event_api.rb +55 -475
  23. data/lib/phronomy/agent/base.rb +351 -514
  24. data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
  25. data/lib/phronomy/agent/context/capability/base.rb +166 -297
  26. data/lib/phronomy/agent/context_assembler.rb +357 -0
  27. data/lib/phronomy/agent/context_candidate.rb +47 -0
  28. data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
  29. data/lib/phronomy/agent/context_importer.rb +217 -0
  30. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
  31. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
  32. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
  33. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +118 -0
  34. data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
  35. data/lib/phronomy/agent/context_plan.rb +25 -0
  36. data/lib/phronomy/agent/context_plan_validator.rb +134 -0
  37. data/lib/phronomy/agent/context_policies/default.rb +53 -0
  38. data/lib/phronomy/agent/context_policy.rb +15 -0
  39. data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
  40. data/lib/phronomy/agent/context_policy_registry.rb +46 -0
  41. data/lib/phronomy/agent/context_request.rb +35 -0
  42. data/lib/phronomy/agent/context_selection_unit.rb +38 -0
  43. data/lib/phronomy/agent/derived_content_spec.rb +34 -0
  44. data/lib/phronomy/agent/execution_coordinator.rb +1122 -0
  45. data/lib/phronomy/agent/immutable.rb +31 -0
  46. data/lib/phronomy/agent/journal_projection.rb +60 -0
  47. data/lib/phronomy/agent/journal_record.rb +67 -0
  48. data/lib/phronomy/agent/llm_call_record.rb +51 -0
  49. data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
  50. data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
  51. data/lib/phronomy/agent/llm_input_patch.rb +21 -0
  52. data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
  53. data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
  54. data/lib/phronomy/agent/ruby_llm_materializer.rb +189 -0
  55. data/lib/phronomy/agent/shared_state.rb +46 -138
  56. data/lib/phronomy/agent/token_budget_resolver.rb +70 -0
  57. data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
  58. data/lib/phronomy/agent/tool_definition_set.rb +55 -0
  59. data/lib/phronomy/agent/tool_invocation.rb +108 -314
  60. data/lib/phronomy/agent.rb +10 -16
  61. data/lib/phronomy/agent_busy_error.rb +5 -0
  62. data/lib/phronomy/canonical_json.rb +136 -0
  63. data/lib/phronomy/configuration.rb +17 -155
  64. data/lib/phronomy/content_store/base.rb +51 -0
  65. data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
  66. data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
  67. data/lib/phronomy/engine/event_loop.rb +3 -0
  68. data/lib/phronomy/engine/runtime.rb +15 -230
  69. data/lib/phronomy/engine/task_group.rb +30 -102
  70. data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
  71. data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
  72. data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
  73. data/lib/phronomy/multi_agent/orchestrator.rb +153 -204
  74. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
  75. data/lib/phronomy/multi_agent/team_coordinator.rb +46 -133
  76. data/lib/phronomy/persistence/in_memory.rb +247 -0
  77. data/lib/phronomy/persistence.rb +39 -0
  78. data/lib/phronomy/tools/agent.rb +14 -36
  79. data/lib/phronomy/vector_store/in_memory.rb +2 -2
  80. data/lib/phronomy/version.rb +1 -1
  81. data/lib/phronomy.rb +9 -115
  82. data/scripts/add_to_h_to_token_doubles.rb +33 -0
  83. data/scripts/add_to_h_unnamed_doubles.rb +27 -0
  84. data/scripts/api_snapshot.rb +1 -12
  85. data/scripts/migrate_spec_agent_definition.rb +108 -0
  86. data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
  87. data/scripts/migrate_spec_inline_pass3.rb +24 -0
  88. metadata +54 -13
  89. data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
  90. data/lib/phronomy/agent/before_completion_context.rb +0 -47
  91. data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
  92. data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
  93. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
  94. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
  95. data/lib/phronomy/knowledge_source.rb +0 -12
  96. data/lib/phronomy/llm_context_window/assembler.rb +0 -191
  97. data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
@@ -4,8 +4,8 @@ module Phronomy
4
4
  module VectorStore
5
5
  # Pure-Ruby in-memory vector store using cosine similarity.
6
6
  #
7
- # Intended for tests, short-lived agents, and Retrieval::Semantic scenarios where
8
- # the message count is small enough that a linear scan is fast enough.
7
+ # Intended for tests, short-lived agents, and small retrieval workloads where
8
+ # the document count is small enough that a linear scan is fast enough.
9
9
  #
10
10
  # @example
11
11
  # store = Phronomy::VectorStore::InMemory.new
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.15.1"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/phronomy.rb CHANGED
@@ -5,25 +5,18 @@ require "ruby_llm"
5
5
  require_relative "phronomy/ruby_llm_patches"
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
- # Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
9
- # ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
10
8
  loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
11
- # RAG: Zeitwerk would infer "Rag" — override to "RAG".
12
9
  loader.inflector.inflect("rag" => "RAG")
13
- # FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
14
- # Phronomy::FSMSession is the top-level cooperative execution engine shared by
15
- # WorkflowRunner, AgentInvocationSessionBuilder, and ToolInvocationSessionBuilder.
16
10
  loader.inflector.inflect("fsm_session" => "FSMSession")
17
- # LLMAdapter: Zeitwerk would infer "LlmAdapter" — override to "LLMAdapter".
18
11
  loader.inflector.inflect("llm_adapter" => "LLMAdapter")
19
- # LLMOperationResult: preserve the LLM acronym for the Agent result carrier.
20
12
  loader.inflector.inflect("llm_operation_result" => "LLMOperationResult")
21
- # LLMAdapter::RubyLLM: "ruby_llm" maps to "RubyLLM" (not "RubyLlm").
22
13
  loader.inflector.inflect("ruby_llm" => "RubyLLM")
23
- # Collapse engine/ so that its contents autoload directly under Phronomy::
24
- # (no Engine:: prefix). e.g. engine/event_loop.rb => Phronomy::EventLoop.
25
- # This allows the execution engine to be organised in its own subdirectory
26
- # without changing any class names or callers.
14
+ loader.inflector.inflect("canonical_json" => "CanonicalJSON")
15
+ loader.inflector.inflect("ruby_llm_materializer" => "RubyLLMMaterializer")
16
+ loader.inflector.inflect("llm_call_record" => "LLMCallRecord")
17
+ loader.inflector.inflect("llm_input_manifest" => "LLMInputManifest")
18
+ loader.inflector.inflect("llm_input_build_context" => "LLMInputBuildContext")
19
+ loader.inflector.inflect("llm_input_patch" => "LLMInputPatch")
27
20
  loader.collapse("#{__dir__}/phronomy/engine")
28
21
  loader.setup
29
22
 
@@ -31,70 +24,25 @@ require_relative "phronomy/version"
31
24
  require_relative "phronomy/token_usage"
32
25
 
33
26
  module Phronomy
34
- # Exception hierarchy
35
27
  class Error < StandardError; end
36
28
  class ParseError < Error; end
37
29
  class RecursionLimitError < Error; end
38
30
  class ToolError < Error; end
39
- # Base error for Phronomy-owned timed boundaries and generic timeout primitives.
40
31
  class TimeoutError < Error; end
41
-
42
32
  class ConfigurationError < Error; end
43
-
44
33
  class HandoffError < Error; end
45
34
 
46
- # Raised when a network or transport layer call fails (e.g. LLM API unreachable,
47
- # MCP server connection refused). Distinguishable from application-level errors
48
- # so callers can apply network-specific retry logic.
49
35
  class TransportError < Error; end
50
-
51
- # Raised when the LLM API returns a rate-limit response (HTTP 429 or equivalent).
52
- # Callers should back off and retry after the indicated delay.
53
36
  class RateLimitError < TransportError; end
54
-
55
- # Raised when the LLM API rejects the request due to an invalid or revoked API key.
56
- # Callers should not retry without fixing the credentials.
57
37
  class AuthenticationError < TransportError; end
58
-
59
- # Raised when the prompt exceeds the model's context window limit.
60
38
  class ContextLengthError < Error; end
61
-
62
- # Raised when a workflow or agent execution is explicitly cancelled.
63
- # Separate from TimeoutError (deadline exceeded) — this is an intentional stop.
64
39
  class CancellationError < Error; end
65
40
 
66
- # Raised when {Agent#invoke} (a synchronous, blocking call) is attempted from
67
- # inside an active scheduler task and +strict_runtime_guards+ is enabled.
68
- #
69
- # Calling a blocking invocation from within a scheduler task stalls the
70
- # scheduler until the inner invocation completes, preventing other tasks from
71
- # making progress (hidden deadlock risk). Use {Agent#invoke_async} followed by
72
- # +#await+ inside scheduler tasks instead.
73
- #
74
- # This error is only raised when:
75
- # Phronomy.configure { |c| c.strict_runtime_guards = true }
76
- #
77
- # By default a warning is logged and execution continues.
78
- #
79
- # @see Phronomy::Runtime.in_scheduler_context?
80
41
  class SchedulerReentrancyError < Error; end
81
-
82
- # Raised when work is submitted to a Runtime whose shutdown has begun, or
83
- # when a Runtime cannot be reset because owned resources are still alive.
84
42
  class RuntimeShutdownError < Error; end
85
-
86
- # Raised when Runtime#shutdown is invoked from inside a Phronomy::Task.
87
43
  class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
88
44
 
89
- # Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
90
- # and the pipeline's combined confidence score falls below the configured threshold.
91
- #
92
- # @example
93
- # rescue Phronomy::LowConfidenceError => e
94
- # puts e.result.confidence # => e.g. 0.45
95
- # puts e.result.output # best-effort answer despite low confidence
96
45
  class LowConfidenceError < Error
97
- # @return [Phronomy::GeneratorVerifier::Result] the untrusted result
98
46
  attr_reader :result
99
47
 
100
48
  def initialize(result)
@@ -103,9 +51,6 @@ module Phronomy
103
51
  end
104
52
  end
105
53
 
106
- # Raised by a {Phronomy::Filter::Base} subclass when the filter rejects a
107
- # value without transforming it (blocking the pipeline).
108
- # @api public
109
54
  class FilterBlockError < Error
110
55
  attr_reader :filter
111
56
 
@@ -115,27 +60,12 @@ module Phronomy
115
60
  end
116
61
  end
117
62
 
118
- # Raised when an operation is submitted to a {BlockingAdapterPool} that has
119
- # already been shut down via {BlockingAdapterPool#shutdown}.
120
63
  class PoolShutdownError < Error; end
121
-
122
- # Raised when a concurrency limit is exceeded and the configured backpressure
123
- # strategy is +:raise+. The caller should back off and retry.
124
64
  class BackpressureError < Error; end
125
65
 
126
- # Raised by {CancellationScope#pop_queue} when the deadline expires before a
127
- # result is available. Extends {TimeoutError} for backwards compatibility.
128
- class ScopeTimeoutError < TimeoutError; end
129
-
130
- # Deprecated compatibility constant. Workflow entry/exit actions are
131
- # synchronous and the Workflow DSL no longer accepts +action_timeout:+.
132
- class ActionTimeoutError < TimeoutError; end
133
-
134
- # Raised when a {Phronomy::WorkflowContext} field is mutated from a thread
135
- # that does not own the context (i.e. not the EventLoop dispatch thread).
136
- # Only raised in EventLoop mode. Use +context.merge(...)+ to produce a new
137
- # context, or deliver updates as +:action_completed+ event payloads
138
- # via {Agent::Base#invoke_async} + {Task#map}.
66
+ # Raised when a WorkflowContext field is mutated from a thread that does not
67
+ # own the context. Deliver asynchronous updates back to the Workflow as later
68
+ # events via Workflow#signal instead of mutating context from worker callbacks.
139
69
  class WorkflowContextOwnershipError < Error; end
140
70
 
141
71
  class << self
@@ -147,36 +77,10 @@ module Phronomy
147
77
  yield configuration
148
78
  end
149
79
 
150
- # Resets the global Phronomy configuration to defaults.
151
- #
152
- # **Intended for test suites only.** Calling this in a production process
153
- # will drop all runtime configuration (tracer, model, tokenizer, etc.)
154
- # globally and immediately affect all subsequent agent and workflow calls.
155
- #
156
- # **Parallel test suites warning:** When tests run in parallel (e.g.
157
- # `parallel_tests` or `parallel_rspec`), +reset_configuration!+ in one
158
- # worker will clear configuration shared with other workers in the same
159
- # process. Prefer process-isolation strategies (forked workers) over
160
- # thread-based parallelism when using this method.
161
- #
162
- # Typical usage in a sequential test suite:
163
- # after { Phronomy.reset_configuration! }
164
80
  def reset_configuration!
165
81
  @configuration = Configuration.new
166
82
  end
167
83
 
168
- # Yields the current {Configuration} object, then restores the original
169
- # configuration on exit (even if the block raises).
170
- #
171
- # Intended for test helpers that need to temporarily override settings
172
- # without permanently mutating the global configuration.
173
- #
174
- # @yield [config] the current {Configuration} instance (mutable)
175
- # @example
176
- # Phronomy.with_configuration do |c|
177
- # c.logger = Logger.new($stdout)
178
- # end
179
- # @api public
180
84
  def with_configuration
181
85
  original = @configuration&.dup
182
86
  yield configuration
@@ -184,16 +88,6 @@ module Phronomy
184
88
  @configuration = original
185
89
  end
186
90
 
187
- # Shuts down and clears the process-wide default Runtime, then resets
188
- # global configuration. Intended for test suites only.
189
- #
190
- # Runtime execution failure and resource cleanup are separate. The
191
- # singleton is cleared when cleanup completed, even if execution failed.
192
- #
193
- # @param timeout [Numeric] maximum graceful wait for Runtime tasks and
194
- # EventLoop shutdown
195
- # @return [Phronomy::Runtime::ShutdownResult]
196
- # @api public
197
91
  def reset_runtime!(timeout: configuration.event_loop_stop_grace_seconds)
198
92
  previous_grace = @configuration&.event_loop_stop_grace_seconds
199
93
  result = Runtime.reset_default!(timeout: timeout)
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Adds to_h to all Tokens-like doubles so ExecutionCoordinator can serialize usage.
5
+
6
+ files = Dir.glob("spec/**/*.rb").sort
7
+
8
+ total = 0
9
+ files.each do |f|
10
+ content = File.read(f)
11
+ modified = content.gsub(
12
+ /double\(("[^"]+"),\s*(input: \d+, output: \d+, cached: \d+, cache_creation: \d+)\)/
13
+ ) do
14
+ name = $1
15
+ attrs = $2
16
+ next $& if $&.include?("to_h")
17
+
18
+ h_entries = attrs.split(",").map(&:strip).map do |pair|
19
+ k, v = pair.split(":").map(&:strip)
20
+ "\"#{k}\" => #{v}"
21
+ end.join(", ")
22
+
23
+ total += 1
24
+ "double(#{name}, #{attrs}, to_h: {#{h_entries}})"
25
+ end
26
+
27
+ if modified != content
28
+ File.write(f, modified)
29
+ puts " patched: #{f}"
30
+ end
31
+ end
32
+
33
+ puts "\nAdded to_h to #{total} Tokens doubles."
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Adds to_h to unnamed (no-string-name) Tokens doubles like double(input: 5, ...)
5
+
6
+ files = Dir.glob("spec/**/*.rb").sort
7
+
8
+ total = 0
9
+ files.each do |f|
10
+ content = File.read(f)
11
+ modified = content.gsub(
12
+ /double\((input: (\d+), output: (\d+), cached: (\d+), cache_creation: (\d+))\)/
13
+ ) do
14
+ attrs, a, b, c, d = $1, $2, $3, $4, $5
15
+ next $& if $&.include?("to_h")
16
+
17
+ total += 1
18
+ "double(#{attrs}, to_h: {\"input\" => #{a}, \"output\" => #{b}, \"cached\" => #{c}, \"cache_creation\" => #{d}})"
19
+ end
20
+
21
+ if modified != content
22
+ File.write(f, modified)
23
+ puts " patched: #{f}"
24
+ end
25
+ end
26
+
27
+ puts "\nAdded to_h to #{total} unnamed Tokens doubles."
@@ -4,23 +4,17 @@
4
4
  # scripts/api_snapshot.rb
5
5
  #
6
6
  # Dumps the public instance methods of all Stable/Beta public API classes to
7
- # JSON. The snapshot is stored in spec/fixtures/api_snapshot.json and is used
7
+ # JSON. The snapshot is stored in spec/fixtures/api_snapshot.json and is used
8
8
  # by spec/phronomy/api_compatibility_spec.rb to detect unintended API removals.
9
9
  #
10
10
  # Usage:
11
- # # Regenerate spec/fixtures/api_snapshot.json (run when intentionally adding
12
- # # or removing public API methods after updating the stability table):
13
11
  # ruby scripts/api_snapshot.rb --write
14
- #
15
- # # Print snapshot to stdout (useful for manual inspection):
16
12
  # ruby scripts/api_snapshot.rb
17
13
 
18
14
  require "json"
19
15
  require "fileutils"
20
16
  require_relative "../lib/phronomy"
21
17
 
22
- # Classes and modules whose public API is tracked.
23
- # Add an entry whenever a new class/module is promoted to Stable or Beta in README.md.
24
18
  PUBLIC_API_ENTRIES = [
25
19
  # Stable
26
20
  Phronomy::Agent::Base,
@@ -37,8 +31,6 @@ PUBLIC_API_ENTRIES = [
37
31
  Phronomy::VectorStore::Base,
38
32
  Phronomy::VectorStore::InMemory,
39
33
  Phronomy::VectorStore::Embeddings::Base,
40
- Phronomy::Agent::Context::Knowledge::Base,
41
- Phronomy::Agent::Context::Knowledge::StaticKnowledge,
42
34
  Phronomy::Tracing::Base,
43
35
  Phronomy::Tracing::NullTracer,
44
36
  Phronomy::Eval::Runner,
@@ -47,7 +39,6 @@ PUBLIC_API_ENTRIES = [
47
39
  Phronomy::Tools::VectorSearch
48
40
  ].freeze
49
41
 
50
- # Baseline methods common to all Ruby objects — excluded from the snapshot.
51
42
  BASELINE_INSTANCE_METHODS = (
52
43
  Object.public_instance_methods |
53
44
  Kernel.public_instance_methods
@@ -60,7 +51,6 @@ BASELINE_CLASS_METHODS = (
60
51
 
61
52
  def snapshot_entry(klass)
62
53
  if klass.instance_of?(Module)
63
- # Module — capture instance methods defined in this module only
64
54
  own_methods = klass.public_instance_methods(false).sort
65
55
  {
66
56
  "name" => klass.name,
@@ -68,7 +58,6 @@ def snapshot_entry(klass)
68
58
  "public_instance_methods" => own_methods
69
59
  }
70
60
  else
71
- # Class — capture public instance methods minus universal baseline
72
61
  instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS).sort
73
62
  class_methods = (klass.public_methods(false) - BASELINE_CLASS_METHODS).sort
74
63
  {
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Migrates spec files to add agent_definition to Agent::Base subclasses.
5
+ # - Named classes: inserts after the class declaration line
6
+ # - Anonymous blocks (do...end): inserts after the opening `do`
7
+ # - Inline anonymous ({ ... }): expands to multi-line with agent_definition
8
+ # Safe: skips classes that already have agent_definition
9
+
10
+ require "fileutils"
11
+
12
+ SPEC_DIR = File.expand_path("../spec", __dir__)
13
+ AGENT_BASE_PATTERN = /Phronomy::Agent::Base/
14
+
15
+ def snake_case(class_name)
16
+ # Remove trailing Agent/Tool suffixes, snake_case the name
17
+ name = class_name.split("::").last
18
+ name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
19
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
20
+ .downcase
21
+ end
22
+
23
+ counter = 0
24
+ files_modified = 0
25
+
26
+ Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
27
+ lines = File.readlines(file, chomp: false)
28
+ new_lines = []
29
+ modified = false
30
+ i = 0
31
+
32
+ while i < lines.length
33
+ line = lines[i]
34
+
35
+ # ── Named class: class FooAgent < Phronomy::Agent::Base ──────────────────
36
+ if line =~ /^(\s*)class\s+(\w+)\s*<\s*(?:::)?Phronomy::Agent::Base\s*$/
37
+ indent = $1
38
+ class_name = $2
39
+ id = snake_case(class_name).sub(/_agent$/, "-agent").tr("_", "-")
40
+ id = "#{id}-agent" unless id.end_with?("-agent")
41
+ new_lines << line
42
+ i += 1
43
+ # Check if agent_definition already present in the class body
44
+ look_ahead = lines[i..]&.take(10) || []
45
+ unless look_ahead.any? { |l| l =~ /agent_definition/ }
46
+ new_lines << "#{indent} agent_definition id: #{id.inspect}, version: 1\n"
47
+ counter += 1
48
+ modified = true
49
+ end
50
+ next
51
+ end
52
+
53
+ # ── Anonymous block (do...end): Class.new(Phronomy::Agent::Base) do ─────
54
+ if line =~ /^(\s*)([\w.]*\s*=\s*)?Class\.new\((?:::)?Phronomy::Agent::Base\)\s+do\s*(\|\w+\|)?\s*$/
55
+ indent = $1
56
+ # Check if agent_definition already present
57
+ look_ahead = lines[i + 1..]&.take(10) || []
58
+ new_lines << line
59
+ i += 1
60
+ unless look_ahead.any? { |l| l =~ /agent_definition/ }
61
+ # Derive indentation from the `do` block body (first non-blank line)
62
+ inner = lines[i..]&.take(5)&.find { |l| l.strip.length > 0 } || ""
63
+ inner_indent = inner.match(/^(\s*)/)[1]
64
+ inner_indent = "#{indent} " if inner_indent.strip.length > 0 && inner_indent.length < indent.length + 2
65
+ new_lines << "#{inner_indent}agent_definition id: \"test-agent-#{counter += 1}\", version: 1\n"
66
+ modified = true
67
+ end
68
+ next
69
+ end
70
+
71
+ # ── Inline anonymous: Class.new(Phronomy::Agent::Base) { ... } ───────────
72
+ if line =~ /^(\s*)((?:[\w.]*\s*=\s*)?)Class\.new\((?:::)?Phronomy::Agent::Base\)\s*\{(.+)\}\s*$/
73
+ indent = $1
74
+ assignment = $2
75
+ body = $3
76
+ # Skip if agent_definition already present
77
+ if /agent_definition/.match?(body)
78
+ new_lines << line
79
+ else
80
+ id = counter += 1
81
+ definition_line = "agent_definition id: \"test-agent-#{id}\", version: 1;"
82
+ new_lines << "#{indent}#{assignment}Class.new(Phronomy::Agent::Base) do\n"
83
+ new_lines << "#{indent} #{definition_line}\n"
84
+ # Rewrite the original body lines, stripping leading spaces
85
+ body.strip.split(";").each do |stmt|
86
+ stmt = stmt.strip
87
+ next if stmt.empty?
88
+ new_lines << "#{indent} #{stmt}\n"
89
+ end
90
+ new_lines << "#{indent}end\n"
91
+ modified = true
92
+ end
93
+ i += 1
94
+ next
95
+ end
96
+
97
+ new_lines << line
98
+ i += 1
99
+ end
100
+
101
+ if modified
102
+ File.write(file, new_lines.join)
103
+ files_modified += 1
104
+ puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
105
+ end
106
+ end
107
+
108
+ puts "\nDone: #{counter} agent_definition insertions in #{files_modified} files."
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Second pass: handles patterns missed by the first script
5
+ # - Class.new(Phronomy::Agent::Base) { ... }.new (inline + .new suffix)
6
+ # - Class.new(Phronomy::Agent::Base).new (no block + .new)
7
+ # - { Class.new(Phronomy::Agent::Base) } (no block, inside outer { })
8
+
9
+ require "fileutils"
10
+
11
+ SPEC_DIR = File.expand_path("../spec", __dir__)
12
+ counter = 200
13
+
14
+ files_modified = 0
15
+
16
+ Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
17
+ content = File.read(file)
18
+ new_content = content.dup
19
+
20
+ # Pattern A: Class.new(Phronomy::Agent::Base) { body }.new
21
+ new_content = new_content.gsub(
22
+ /Class\.new\(Phronomy::Agent::Base\) \{([^}]*)\}\.new/
23
+ ) do
24
+ body = $1
25
+ next $& if body.include?("agent_definition")
26
+ counter += 1
27
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1;#{body}}.new"
28
+ end
29
+
30
+ # Pattern B: Class.new(Phronomy::Agent::Base).new (no block, chained .new)
31
+ new_content = new_content.gsub(
32
+ /Class\.new\(Phronomy::Agent::Base\)\.new\b/
33
+ ) do
34
+ counter += 1
35
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 }.new"
36
+ end
37
+
38
+ # Pattern C: { Class.new(Phronomy::Agent::Base) } (no block, inside outer block)
39
+ new_content = new_content.gsub(
40
+ "{ Class.new(Phronomy::Agent::Base) }"
41
+ ) do
42
+ counter += 1
43
+ "{ Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 } }"
44
+ end
45
+
46
+ if new_content != content
47
+ File.write(file, new_content)
48
+ files_modified += 1
49
+ puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
50
+ end
51
+ end
52
+
53
+ puts "\nSecond pass done: #{files_modified} files modified."
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Adds agent_definition to single-line inline anonymous blocks like:
5
+ # Class.new(Phronomy::Agent::Base) { model "test-model" }
6
+
7
+ counter = 300
8
+ files = Dir.glob("spec/**/*.rb").sort
9
+
10
+ files.each do |f|
11
+ content = File.read(f)
12
+ modified = content.gsub(
13
+ /Class\.new\(Phronomy::Agent::Base\) \{ ([^{}]+) \}/
14
+ ) do
15
+ body = $1
16
+ next $& if body.include?("agent_definition")
17
+ counter += 1
18
+ "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1; #{body} }"
19
+ end
20
+ if modified != content
21
+ File.write(f, modified)
22
+ puts " patched: #{f}"
23
+ end
24
+ end