phronomy 0.5.4 → 0.7.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +21 -0
  3. data/CHANGELOG.md +379 -0
  4. data/CONTRIBUTING.md +102 -0
  5. data/README.md +262 -48
  6. data/RELEASE_CHECKLIST.md +86 -0
  7. data/SECURITY.md +80 -0
  8. data/benchmark/baseline.json +9 -0
  9. data/benchmark/bench_agent_invoke.rb +105 -0
  10. data/benchmark/bench_context_assembler.rb +46 -0
  11. data/benchmark/bench_regression.rb +171 -0
  12. data/benchmark/bench_token_estimator.rb +44 -0
  13. data/benchmark/bench_tool_schema.rb +69 -0
  14. data/benchmark/bench_vector_store.rb +39 -0
  15. data/benchmark/bench_workflow.rb +55 -0
  16. data/benchmark/run_all.rb +118 -0
  17. data/docs/decisions/001-rubyllm-as-provider-layer.md +42 -0
  18. data/docs/decisions/002-workflow-context-immutability.md +42 -0
  19. data/docs/decisions/003-event-loop-singleton.md +48 -0
  20. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +51 -0
  21. data/docs/decisions/005-static-knowledge-class-level-cache.md +45 -0
  22. data/docs/decisions/006-no-built-in-guardrails.md +48 -0
  23. data/docs/decisions/007-mcp-is-beta-stability.md +51 -0
  24. data/docs/decisions/008-orchestrator-uses-os-threads.md +52 -0
  25. data/docs/decisions/009-state-store-abstraction.md +141 -0
  26. data/lib/phronomy/agent/base.rb +281 -13
  27. data/lib/phronomy/agent/before_completion_context.rb +1 -0
  28. data/lib/phronomy/agent/checkpoint.rb +1 -0
  29. data/lib/phronomy/agent/concerns/before_completion.rb +6 -0
  30. data/lib/phronomy/agent/concerns/error_translation.rb +45 -0
  31. data/lib/phronomy/agent/concerns/guardrailable.rb +3 -0
  32. data/lib/phronomy/agent/concerns/retryable.rb +12 -1
  33. data/lib/phronomy/agent/concerns/suspendable.rb +4 -0
  34. data/lib/phronomy/agent/fsm.rb +180 -0
  35. data/lib/phronomy/agent/handoff.rb +3 -0
  36. data/lib/phronomy/agent/orchestrator.rb +123 -11
  37. data/lib/phronomy/agent/parallel_tool_chat.rb +92 -0
  38. data/lib/phronomy/agent/react_agent.rb +8 -6
  39. data/lib/phronomy/agent/runner.rb +2 -0
  40. data/lib/phronomy/agent/shared_state.rb +11 -0
  41. data/lib/phronomy/agent/suspend_signal.rb +2 -0
  42. data/lib/phronomy/agent/team_coordinator.rb +17 -5
  43. data/lib/phronomy/cancellation_token.rb +92 -0
  44. data/lib/phronomy/configuration.rb +32 -2
  45. data/lib/phronomy/context/assembler.rb +6 -0
  46. data/lib/phronomy/context/compaction_context.rb +2 -0
  47. data/lib/phronomy/context/context_version_cache.rb +2 -0
  48. data/lib/phronomy/context/token_budget.rb +3 -0
  49. data/lib/phronomy/context/token_estimator.rb +9 -2
  50. data/lib/phronomy/context/trigger_context.rb +1 -0
  51. data/lib/phronomy/context/trim_context.rb +4 -0
  52. data/lib/phronomy/context.rb +0 -1
  53. data/lib/phronomy/embeddings/base.rb +5 -2
  54. data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +6 -2
  55. data/lib/phronomy/eval/comparison.rb +2 -0
  56. data/lib/phronomy/eval/dataset.rb +4 -0
  57. data/lib/phronomy/eval/metrics.rb +6 -0
  58. data/lib/phronomy/eval/runner.rb +2 -0
  59. data/lib/phronomy/eval/scorer/base.rb +1 -0
  60. data/lib/phronomy/eval/scorer/exact_match.rb +2 -0
  61. data/lib/phronomy/eval/scorer/includes_scorer.rb +2 -0
  62. data/lib/phronomy/eval/scorer/llm_judge.rb +2 -0
  63. data/lib/phronomy/event.rb +14 -0
  64. data/lib/phronomy/event_loop.rb +254 -0
  65. data/lib/phronomy/fsm_session.rb +201 -0
  66. data/lib/phronomy/generator_verifier.rb +24 -22
  67. data/lib/phronomy/guardrail/base.rb +3 -0
  68. data/lib/phronomy/guardrail.rb +0 -1
  69. data/lib/phronomy/knowledge_source/base.rb +6 -2
  70. data/lib/phronomy/knowledge_source/entity_knowledge.rb +7 -2
  71. data/lib/phronomy/knowledge_source/rag_knowledge.rb +8 -4
  72. data/lib/phronomy/knowledge_source/static_knowledge.rb +7 -2
  73. data/lib/phronomy/loader/base.rb +1 -0
  74. data/lib/phronomy/loader/csv_loader.rb +2 -0
  75. data/lib/phronomy/loader/markdown_loader.rb +2 -0
  76. data/lib/phronomy/loader/plain_text_loader.rb +1 -0
  77. data/lib/phronomy/output_parser/base.rb +1 -0
  78. data/lib/phronomy/output_parser/json_parser.rb +22 -3
  79. data/lib/phronomy/output_parser/structured_parser.rb +2 -0
  80. data/lib/phronomy/prompt_template.rb +5 -0
  81. data/lib/phronomy/runnable.rb +20 -3
  82. data/lib/phronomy/splitter/base.rb +2 -0
  83. data/lib/phronomy/splitter/fixed_size_splitter.rb +2 -0
  84. data/lib/phronomy/splitter/recursive_splitter.rb +2 -0
  85. data/lib/phronomy/state_store/base.rb +48 -0
  86. data/lib/phronomy/state_store/in_memory.rb +62 -0
  87. data/lib/phronomy/tool/agent_tool.rb +1 -0
  88. data/lib/phronomy/tool/base.rb +189 -27
  89. data/lib/phronomy/tool/mcp_tool.rb +68 -13
  90. data/lib/phronomy/tracing/base.rb +3 -0
  91. data/lib/phronomy/tracing/langfuse_tracer.rb +2 -0
  92. data/lib/phronomy/tracing/open_telemetry_tracer.rb +2 -0
  93. data/lib/phronomy/vector_store/base.rb +33 -7
  94. data/lib/phronomy/vector_store/in_memory.rb +16 -7
  95. data/lib/phronomy/vector_store/pgvector.rb +40 -9
  96. data/lib/phronomy/vector_store/redis_search.rb +29 -8
  97. data/lib/phronomy/version.rb +1 -1
  98. data/lib/phronomy/workflow.rb +175 -74
  99. data/lib/phronomy/workflow_context.rb +55 -5
  100. data/lib/phronomy/workflow_runner.rb +197 -114
  101. data/lib/phronomy.rb +74 -1
  102. data/scripts/api_snapshot.rb +91 -0
  103. data/scripts/check_api_annotations.rb +68 -0
  104. data/scripts/check_private_enforcement.rb +93 -0
  105. data/scripts/check_readme_runnable.rb +98 -0
  106. data/scripts/run_mutation.sh +46 -0
  107. metadata +50 -6
  108. data/lib/phronomy/context/builder.rb +0 -92
  109. data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +0 -100
  110. data/lib/phronomy/guardrail/builtin/prompt_injection_detector.rb +0 -67
  111. data/lib/phronomy/guardrail/builtin.rb +0 -16
data/lib/phronomy.rb CHANGED
@@ -8,6 +8,10 @@ loader = Zeitwerk::Loader.for_gem
8
8
  # Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
9
9
  # ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
10
10
  loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
11
+ # FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
12
+ loader.inflector.inflect("fsm_session" => "FSMSession")
13
+ # AgentFSM: Zeitwerk would infer "Fsm" — override to "FSM".
14
+ loader.inflector.inflect("fsm" => "FSM")
11
15
  loader.setup
12
16
 
13
17
  require_relative "phronomy/version"
@@ -19,11 +23,33 @@ module Phronomy
19
23
  class ParseError < Error; end
20
24
  class RecursionLimitError < Error; end
21
25
  class ToolError < Error; end
26
+ # Raised when an agent invocation exceeds the timeout set via +invoke_timeout+.
27
+ class TimeoutError < Error; end
22
28
 
23
29
  class ConfigurationError < Error; end
24
30
 
25
31
  class HandoffError < Error; end
26
32
 
33
+ # Raised when a network or transport layer call fails (e.g. LLM API unreachable,
34
+ # MCP server connection refused). Distinguishable from application-level errors
35
+ # so callers can apply network-specific retry logic.
36
+ class TransportError < Error; end
37
+
38
+ # Raised when the LLM API returns a rate-limit response (HTTP 429 or equivalent).
39
+ # Callers should back off and retry after the indicated delay.
40
+ class RateLimitError < TransportError; end
41
+
42
+ # Raised when the LLM API rejects the request due to an invalid or revoked API key.
43
+ # Callers should not retry without fixing the credentials.
44
+ class AuthenticationError < TransportError; end
45
+
46
+ # Raised when the prompt exceeds the model's context window limit.
47
+ class ContextLengthError < Error; end
48
+
49
+ # Raised when a workflow or agent execution is explicitly cancelled.
50
+ # Separate from TimeoutError (deadline exceeded) — this is an intentional stop.
51
+ class CancellationError < Error; end
52
+
27
53
  # Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
28
54
  # and the pipeline's combined confidence score falls below the configured threshold.
29
55
  #
@@ -59,9 +85,56 @@ module Phronomy
59
85
  yield configuration
60
86
  end
61
87
 
62
- # Resets configuration; primarily used in tests.
88
+ # Resets the global Phronomy configuration to defaults.
89
+ #
90
+ # **Intended for test suites only.** Calling this in a production process
91
+ # will drop all runtime configuration (tracer, model, tokenizer, etc.)
92
+ # globally and immediately affect all subsequent agent and workflow calls.
93
+ #
94
+ # **Parallel test suites warning:** When tests run in parallel (e.g.
95
+ # `parallel_tests` or `parallel_rspec`), +reset_configuration!+ in one
96
+ # worker will clear configuration shared with other workers in the same
97
+ # process. Prefer process-isolation strategies (forked workers) over
98
+ # thread-based parallelism when using this method.
99
+ #
100
+ # Typical usage in a sequential test suite:
101
+ # after { Phronomy.reset_configuration! }
63
102
  def reset_configuration!
64
103
  @configuration = Configuration.new
65
104
  end
105
+
106
+ # Yields the current {Configuration} object, then restores the original
107
+ # configuration on exit (even if the block raises).
108
+ #
109
+ # Intended for test helpers that need to temporarily override settings
110
+ # without permanently mutating the global configuration.
111
+ #
112
+ # @yield [config] the current {Configuration} instance (mutable)
113
+ # @example
114
+ # Phronomy.with_configuration do |c|
115
+ # c.logger = Logger.new($stdout)
116
+ # end
117
+ # @api public
118
+ def with_configuration
119
+ original = @configuration&.dup
120
+ yield configuration
121
+ ensure
122
+ @configuration = original
123
+ end
124
+
125
+ # Resets all Phronomy runtime state: configuration and the EventLoop
126
+ # singleton (if running).
127
+ #
128
+ # **Intended for test suites only.** Stops any running EventLoop thread,
129
+ # clears the EventLoop singleton, and resets configuration to defaults.
130
+ # Call once before/after each example to ensure test isolation.
131
+ #
132
+ # @example
133
+ # config.around { |ex| Phronomy.reset_runtime! ; ex.run ; Phronomy.reset_runtime! }
134
+ # @api public
135
+ def reset_runtime!
136
+ Phronomy::EventLoop.reset!
137
+ @configuration = Configuration.new
138
+ end
66
139
  end
67
140
  end
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # scripts/api_snapshot.rb
5
+ #
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
8
+ # by spec/phronomy/api_compatibility_spec.rb to detect unintended API removals.
9
+ #
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
+ # ruby scripts/api_snapshot.rb --write
14
+ #
15
+ # # Print snapshot to stdout (useful for manual inspection):
16
+ # ruby scripts/api_snapshot.rb
17
+
18
+ require "json"
19
+ require "fileutils"
20
+ require_relative "../lib/phronomy"
21
+
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
+ PUBLIC_API_ENTRIES = [
25
+ # Stable
26
+ Phronomy::Agent::Base,
27
+ Phronomy::Tool::Base,
28
+ Phronomy::Workflow,
29
+ Phronomy::WorkflowContext,
30
+ Phronomy::Runnable,
31
+ Phronomy::PromptTemplate,
32
+ # Beta
33
+ Phronomy::Agent::ReactAgent,
34
+ Phronomy::Agent::Orchestrator,
35
+ Phronomy::Agent::TeamCoordinator,
36
+ Phronomy::Guardrail::InputGuardrail,
37
+ Phronomy::Guardrail::OutputGuardrail,
38
+ Phronomy::VectorStore::Base,
39
+ Phronomy::VectorStore::InMemory,
40
+ Phronomy::Embeddings::Base,
41
+ Phronomy::KnowledgeSource::Base,
42
+ Phronomy::KnowledgeSource::StaticKnowledge,
43
+ Phronomy::KnowledgeSource::RAGKnowledge,
44
+ Phronomy::Tracing::Base,
45
+ Phronomy::Tracing::NullTracer,
46
+ Phronomy::Eval::Runner
47
+ ].freeze
48
+
49
+ # Baseline methods common to all Ruby objects — excluded from the snapshot.
50
+ BASELINE_INSTANCE_METHODS = (
51
+ Object.public_instance_methods |
52
+ Kernel.public_instance_methods
53
+ ).uniq.freeze
54
+
55
+ BASELINE_CLASS_METHODS = (
56
+ Class.public_methods |
57
+ Module.public_methods
58
+ ).uniq.freeze
59
+
60
+ def snapshot_entry(klass)
61
+ if klass.instance_of?(Module)
62
+ # Module — capture instance methods defined in this module only
63
+ own_methods = klass.public_instance_methods(false).sort
64
+ {
65
+ "name" => klass.name,
66
+ "type" => "module",
67
+ "public_instance_methods" => own_methods
68
+ }
69
+ else
70
+ # Class — capture public instance methods minus universal baseline
71
+ instance_methods = (klass.public_instance_methods - BASELINE_INSTANCE_METHODS).sort
72
+ class_methods = (klass.public_methods(false) - BASELINE_CLASS_METHODS).sort
73
+ {
74
+ "name" => klass.name,
75
+ "type" => "class",
76
+ "public_instance_methods" => instance_methods,
77
+ "public_class_methods" => class_methods
78
+ }
79
+ end
80
+ end
81
+
82
+ snapshot = PUBLIC_API_ENTRIES.map { |entry| snapshot_entry(entry) }
83
+
84
+ if ARGV.include?("--write")
85
+ path = File.expand_path("../spec/fixtures/api_snapshot.json", __dir__)
86
+ FileUtils.mkdir_p(File.dirname(path))
87
+ File.write(path, JSON.pretty_generate(snapshot) + "\n")
88
+ puts "Wrote #{path}"
89
+ else
90
+ puts JSON.pretty_generate(snapshot)
91
+ end
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # check_api_annotations.rb
5
+ #
6
+ # Verifies that every YARD-documented public method in lib/ carries either
7
+ # "@api public" or "@api private".
8
+ #
9
+ # A method is considered "YARD-documented" when its preceding comment block
10
+ # contains at least one @param, @return, @raise, @yield, @example, or
11
+ # @overload tag. Methods with only a plain prose description (no @ tags)
12
+ # are exempt.
13
+ #
14
+ # Usage (run from the phronomy/ repository root):
15
+ # ruby scripts/check_api_annotations.rb
16
+ #
17
+ # Exit codes:
18
+ # 0 — all documented methods carry @api annotations
19
+ # 1 — one or more documented methods are missing @api annotations
20
+
21
+ lib_dir = File.expand_path("../lib", __dir__)
22
+
23
+ unless File.directory?(lib_dir)
24
+ warn "ERROR: lib directory not found at #{lib_dir}"
25
+ exit 1
26
+ end
27
+
28
+ errors = []
29
+
30
+ Dir.glob(File.join(lib_dir, "**", "*.rb")).sort.each do |file|
31
+ lines = File.readlines(file)
32
+
33
+ lines.each_with_index do |line, i|
34
+ next unless line.match?(/^\s*def\s+\w/)
35
+
36
+ # Collect the contiguous comment block immediately above this def.
37
+ comment_lines = []
38
+ j = i - 1
39
+ while j >= 0 && lines[j].match?(/^\s*#/)
40
+ comment_lines.unshift(lines[j])
41
+ j -= 1
42
+ end
43
+
44
+ next if comment_lines.empty?
45
+
46
+ comment = comment_lines.join
47
+
48
+ # Only lint methods that carry at least one YARD type tag.
49
+ next unless comment.match?(/#[ \t]+@(param|return|raise|yield|example|overload)/)
50
+
51
+ # Pass if an @api tag is already present.
52
+ next if comment.match?(/#[ \t]+@api[ \t]+(public|private)/)
53
+
54
+ rel_path = file.sub("#{lib_dir}/../", "")
55
+ m = line.match(/def\s+(\w+[!?=]?)/)
56
+ method_name = m ? m[1] : "unknown"
57
+ errors << "#{rel_path}:#{i + 1} def #{method_name} (missing @api public or @api private)"
58
+ end
59
+ end
60
+
61
+ if errors.empty?
62
+ puts "OK: all YARD-documented methods carry @api annotations"
63
+ exit 0
64
+ else
65
+ puts "FAIL: #{errors.size} method(s) missing @api annotation:"
66
+ errors.each { |e| puts " #{e}" }
67
+ exit 1
68
+ end
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # check_private_enforcement.rb
5
+ #
6
+ # Verifies that every instance method annotated @api private in lib/ is
7
+ # actually non-public at the Ruby level (i.e., NOT in Module#public_instance_methods).
8
+ #
9
+ # Class methods (def self.xxx) are excluded from this check because their
10
+ # visibility is managed separately on the singleton class and rarely causes
11
+ # accidental public exposure to consumers.
12
+ #
13
+ # Usage (run from the phronomy/ repository root):
14
+ # bundle exec ruby scripts/check_private_enforcement.rb
15
+ #
16
+ # Exit codes:
17
+ # 0 — all @api private instance methods are non-public (or have no Ruby def)
18
+ # 1 — one or more @api private instance methods are exposed as public
19
+
20
+ require "bundler/setup"
21
+ require_relative "../lib/phronomy"
22
+
23
+ lib_dir = File.expand_path("../lib", __dir__)
24
+
25
+ unless File.directory?(lib_dir)
26
+ warn "ERROR: lib directory not found at #{lib_dir}"
27
+ exit 1
28
+ end
29
+
30
+ # Step 1: Collect instance methods annotated @api private via static analysis.
31
+ api_private_entries = []
32
+
33
+ Dir.glob(File.join(lib_dir, "**", "*.rb")).sort.each do |file|
34
+ lines = File.readlines(file)
35
+
36
+ lines.each_with_index do |line, i|
37
+ next unless line.match?(/^\s*#\s*@api\s+private\s*$/)
38
+
39
+ # Advance past any further comment or blank lines to reach the def.
40
+ j = i + 1
41
+ j += 1 while j < lines.size && lines[j].match?(/^\s*(#|$)/)
42
+ next unless j < lines.size
43
+
44
+ # Skip class-level methods — they live on the singleton class, not as
45
+ # public instance methods accessible to consumers.
46
+ next if lines[j].match?(/def\s+self\./)
47
+
48
+ # Match both plain def and "private def".
49
+ m = lines[j].match(/^\s*(?:private\s+)?def\s+(\w+[!?=]?)/)
50
+ next unless m
51
+
52
+ rel_path = file.sub("#{lib_dir}/../", "")
53
+ api_private_entries << {name: m[1].to_sym, file: rel_path, line: j + 1}
54
+ end
55
+ end
56
+
57
+ if api_private_entries.empty?
58
+ puts "No @api private instance methods found."
59
+ exit 0
60
+ end
61
+
62
+ # Step 2: Build a map of publicly exposed instance methods across all
63
+ # Phronomy-namespaced modules/classes (own methods only, no inheritance).
64
+ all_phronomy_modules = ObjectSpace.each_object(Module).select do |mod|
65
+ mod.name&.start_with?("Phronomy")
66
+ end
67
+
68
+ public_exposure_map = {}
69
+ all_phronomy_modules.each do |mod|
70
+ mod.public_instance_methods(false).each do |meth|
71
+ (public_exposure_map[meth] ||= []) << mod.name
72
+ end
73
+ end
74
+
75
+ # Step 3: Report violations — @api private methods that are still public.
76
+ errors = []
77
+
78
+ api_private_entries.each do |entry|
79
+ exposing_modules = public_exposure_map[entry[:name]]
80
+ next unless exposing_modules
81
+
82
+ errors << "#{entry[:file]}:#{entry[:line]} def #{entry[:name]}" \
83
+ " (annotated @api private but public in: #{exposing_modules.join(", ")})"
84
+ end
85
+
86
+ if errors.empty?
87
+ puts "OK: all #{api_private_entries.size} @api private instance methods are non-public."
88
+ exit 0
89
+ else
90
+ warn "ERROR: #{errors.size} @api private instance method(s) are exposed as public:"
91
+ errors.each { |e| warn " #{e}" }
92
+ exit 1
93
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
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
+
14
+ require "tempfile"
15
+ require "open3"
16
+
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"
28
+
29
+ # Patch invoke methods to return canned responses instead of calling the LLM.
30
+ module Phronomy
31
+ module Agent
32
+ class Base
33
+ def invoke(input = nil, **)
34
+ {output: "ci-stub-output", messages: []}
35
+ end
36
+ end
37
+
38
+ class Runner
39
+ def invoke(input = nil, **)
40
+ {output: "ci-stub-output", agent: nil, messages: []}
41
+ end
42
+ end
43
+ end
44
+
45
+ module Chain
46
+ class LLMChain
47
+ def invoke(vars = {})
48
+ "ci-stub-chain"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ # --- end CI preamble ---
54
+
55
+ RUBY
56
+
57
+ readme = File.read(README_PATH)
58
+
59
+ # Match opening fence with 'runnable' annotation: ```ruby runnable
60
+ blocks = readme.scan(/^```ruby runnable\n(.*?)^```/m).map.with_index(1) { |(code), i| [i, code] }
61
+
62
+ if blocks.empty?
63
+ puts "No 'ruby runnable' blocks found in README.md."
64
+ exit 0
65
+ end
66
+
67
+ puts "Checking #{blocks.size} runnable Ruby block(s) in README.md..."
68
+
69
+ failures = []
70
+
71
+ blocks.each do |index, code|
72
+ Tempfile.create(["readme_runnable_#{index}", ".rb"]) do |f|
73
+ f.write(PREAMBLE)
74
+ f.write(code)
75
+ f.flush
76
+
77
+ out, err, status = Open3.capture3(RbConfig.ruby, f.path)
78
+ combined = (out + err).gsub(f.path, "block ##{index}")
79
+
80
+ if status.success?
81
+ puts " OK block ##{index}"
82
+ else
83
+ failures << index
84
+ puts " FAIL block ##{index}"
85
+ # Print at most 15 lines of output to keep CI logs readable.
86
+ puts combined.lines.first(15).join
87
+ end
88
+ end
89
+ end
90
+
91
+ puts
92
+ if failures.empty?
93
+ puts "All #{blocks.size} runnable block(s) passed."
94
+ exit 0
95
+ else
96
+ puts "#{failures.size} block(s) failed: #{failures.join(", ")}"
97
+ exit 1
98
+ end
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/run_mutation.sh — Run mutation tests on core Phronomy domain classes.
3
+ #
4
+ # Usage:
5
+ # bash scripts/run_mutation.sh [SUBJECT_PATTERN]
6
+ #
7
+ # SUBJECT_PATTERN (optional): restrict to a specific subject, e.g. "Phronomy::WorkflowContext"
8
+ # When omitted, all subjects listed in .mutant.yml are tested.
9
+ #
10
+ # Requires mutant-rspec (in Gemfile development group):
11
+ # gem "mutant-rspec", "~> 0.15.1"
12
+ #
13
+ # Target: mutation score >= 80% for each listed subject.
14
+ # Baseline scores (as of initial run):
15
+ # Phronomy::WorkflowContext 84.85%
16
+ # Phronomy::Tool::Base 55.74%
17
+ #
18
+ # Note: mutation testing is slow (~1-5 min per subject). Run locally or via
19
+ # the nightly-mutation GitHub Actions workflow.
20
+
21
+ set -euo pipefail
22
+
23
+ cd "$(dirname "$0")/.."
24
+
25
+ if ! bundle exec mutant --version &>/dev/null; then
26
+ echo "ERROR: mutant is not available. Run: bundle install"
27
+ exit 1
28
+ fi
29
+
30
+ SUBJECT="${1:-}"
31
+
32
+ echo "=== Phronomy Mutation Test ==="
33
+ echo "Date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
34
+ echo "Ruby: $(ruby --version)"
35
+ echo "Mutant: $(bundle exec mutant --version 2>&1 | grep -v warning | head -1)"
36
+ echo ""
37
+
38
+ if [[ -n "$SUBJECT" ]]; then
39
+ echo "Subject: $SUBJECT"
40
+ echo ""
41
+ bundle exec mutant run -- "$SUBJECT"
42
+ else
43
+ echo "Subjects: all (see .mutant.yml)"
44
+ echo ""
45
+ bundle exec mutant run
46
+ fi
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-20 00:00:00.000000000 Z
11
+ date: 2026-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.3'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.3'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: zeitwerk
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -31,6 +37,9 @@ dependencies:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: '2.6'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '3'
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +47,9 @@ dependencies:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
49
  version: '2.6'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '3'
41
53
  - !ruby/object:Gem::Dependency
42
54
  name: state_machines
43
55
  requirement: !ruby/object:Gem::Requirement
@@ -60,30 +72,55 @@ executables: []
60
72
  extensions: []
61
73
  extra_rdoc_files: []
62
74
  files:
75
+ - ".mutant.yml"
63
76
  - ".yardopts"
64
77
  - CHANGELOG.md
78
+ - CONTRIBUTING.md
65
79
  - README.md
80
+ - RELEASE_CHECKLIST.md
66
81
  - Rakefile
82
+ - SECURITY.md
83
+ - benchmark/baseline.json
84
+ - benchmark/bench_agent_invoke.rb
85
+ - benchmark/bench_context_assembler.rb
86
+ - benchmark/bench_regression.rb
87
+ - benchmark/bench_token_estimator.rb
88
+ - benchmark/bench_tool_schema.rb
89
+ - benchmark/bench_vector_store.rb
90
+ - benchmark/bench_workflow.rb
91
+ - benchmark/run_all.rb
92
+ - docs/decisions/001-rubyllm-as-provider-layer.md
93
+ - docs/decisions/002-workflow-context-immutability.md
94
+ - docs/decisions/003-event-loop-singleton.md
95
+ - docs/decisions/004-invoke-timeout-is-not-cancellation.md
96
+ - docs/decisions/005-static-knowledge-class-level-cache.md
97
+ - docs/decisions/006-no-built-in-guardrails.md
98
+ - docs/decisions/007-mcp-is-beta-stability.md
99
+ - docs/decisions/008-orchestrator-uses-os-threads.md
100
+ - docs/decisions/009-state-store-abstraction.md
67
101
  - lib/phronomy.rb
68
102
  - lib/phronomy/agent.rb
69
103
  - lib/phronomy/agent/base.rb
70
104
  - lib/phronomy/agent/before_completion_context.rb
71
105
  - lib/phronomy/agent/checkpoint.rb
72
106
  - lib/phronomy/agent/concerns/before_completion.rb
107
+ - lib/phronomy/agent/concerns/error_translation.rb
73
108
  - lib/phronomy/agent/concerns/guardrailable.rb
74
109
  - lib/phronomy/agent/concerns/retryable.rb
75
110
  - lib/phronomy/agent/concerns/suspendable.rb
111
+ - lib/phronomy/agent/fsm.rb
76
112
  - lib/phronomy/agent/handoff.rb
77
113
  - lib/phronomy/agent/orchestrator.rb
114
+ - lib/phronomy/agent/parallel_tool_chat.rb
78
115
  - lib/phronomy/agent/react_agent.rb
79
116
  - lib/phronomy/agent/runner.rb
80
117
  - lib/phronomy/agent/shared_state.rb
81
118
  - lib/phronomy/agent/suspend_signal.rb
82
119
  - lib/phronomy/agent/team_coordinator.rb
120
+ - lib/phronomy/cancellation_token.rb
83
121
  - lib/phronomy/configuration.rb
84
122
  - lib/phronomy/context.rb
85
123
  - lib/phronomy/context/assembler.rb
86
- - lib/phronomy/context/builder.rb
87
124
  - lib/phronomy/context/compaction_context.rb
88
125
  - lib/phronomy/context/context_version_cache.rb
89
126
  - lib/phronomy/context/token_budget.rb
@@ -105,12 +142,12 @@ files:
105
142
  - lib/phronomy/eval/scorer/exact_match.rb
106
143
  - lib/phronomy/eval/scorer/includes_scorer.rb
107
144
  - lib/phronomy/eval/scorer/llm_judge.rb
145
+ - lib/phronomy/event.rb
146
+ - lib/phronomy/event_loop.rb
147
+ - lib/phronomy/fsm_session.rb
108
148
  - lib/phronomy/generator_verifier.rb
109
149
  - lib/phronomy/guardrail.rb
110
150
  - lib/phronomy/guardrail/base.rb
111
- - lib/phronomy/guardrail/builtin.rb
112
- - lib/phronomy/guardrail/builtin/pii_pattern_detector.rb
113
- - lib/phronomy/guardrail/builtin/prompt_injection_detector.rb
114
151
  - lib/phronomy/guardrail/input_guardrail.rb
115
152
  - lib/phronomy/guardrail/output_guardrail.rb
116
153
  - lib/phronomy/knowledge_source.rb
@@ -134,6 +171,8 @@ files:
134
171
  - lib/phronomy/splitter/base.rb
135
172
  - lib/phronomy/splitter/fixed_size_splitter.rb
136
173
  - lib/phronomy/splitter/recursive_splitter.rb
174
+ - lib/phronomy/state_store/base.rb
175
+ - lib/phronomy/state_store/in_memory.rb
137
176
  - lib/phronomy/token_usage.rb
138
177
  - lib/phronomy/tool.rb
139
178
  - lib/phronomy/tool/agent_tool.rb
@@ -153,7 +192,12 @@ files:
153
192
  - lib/phronomy/workflow.rb
154
193
  - lib/phronomy/workflow_context.rb
155
194
  - lib/phronomy/workflow_runner.rb
195
+ - scripts/api_snapshot.rb
196
+ - scripts/check_api_annotations.rb
197
+ - scripts/check_private_enforcement.rb
156
198
  - scripts/check_readme_ruby.rb
199
+ - scripts/check_readme_runnable.rb
200
+ - scripts/run_mutation.sh
157
201
  - sig/phronomy.rbs
158
202
  homepage: https://github.com/Raizo-TCS/phronomy
159
203
  licenses: