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.
- checksums.yaml +4 -4
- data/.mutant.yml +21 -0
- data/CHANGELOG.md +379 -0
- data/CONTRIBUTING.md +102 -0
- data/README.md +262 -48
- data/RELEASE_CHECKLIST.md +86 -0
- data/SECURITY.md +80 -0
- data/benchmark/baseline.json +9 -0
- data/benchmark/bench_agent_invoke.rb +105 -0
- data/benchmark/bench_context_assembler.rb +46 -0
- data/benchmark/bench_regression.rb +171 -0
- data/benchmark/bench_token_estimator.rb +44 -0
- data/benchmark/bench_tool_schema.rb +69 -0
- data/benchmark/bench_vector_store.rb +39 -0
- data/benchmark/bench_workflow.rb +55 -0
- data/benchmark/run_all.rb +118 -0
- data/docs/decisions/001-rubyllm-as-provider-layer.md +42 -0
- data/docs/decisions/002-workflow-context-immutability.md +42 -0
- data/docs/decisions/003-event-loop-singleton.md +48 -0
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +51 -0
- data/docs/decisions/005-static-knowledge-class-level-cache.md +45 -0
- data/docs/decisions/006-no-built-in-guardrails.md +48 -0
- data/docs/decisions/007-mcp-is-beta-stability.md +51 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +52 -0
- data/docs/decisions/009-state-store-abstraction.md +141 -0
- data/lib/phronomy/agent/base.rb +281 -13
- data/lib/phronomy/agent/before_completion_context.rb +1 -0
- data/lib/phronomy/agent/checkpoint.rb +1 -0
- data/lib/phronomy/agent/concerns/before_completion.rb +6 -0
- data/lib/phronomy/agent/concerns/error_translation.rb +45 -0
- data/lib/phronomy/agent/concerns/guardrailable.rb +3 -0
- data/lib/phronomy/agent/concerns/retryable.rb +12 -1
- data/lib/phronomy/agent/concerns/suspendable.rb +4 -0
- data/lib/phronomy/agent/fsm.rb +180 -0
- data/lib/phronomy/agent/handoff.rb +3 -0
- data/lib/phronomy/agent/orchestrator.rb +123 -11
- data/lib/phronomy/agent/parallel_tool_chat.rb +92 -0
- data/lib/phronomy/agent/react_agent.rb +8 -6
- data/lib/phronomy/agent/runner.rb +2 -0
- data/lib/phronomy/agent/shared_state.rb +11 -0
- data/lib/phronomy/agent/suspend_signal.rb +2 -0
- data/lib/phronomy/agent/team_coordinator.rb +17 -5
- data/lib/phronomy/cancellation_token.rb +92 -0
- data/lib/phronomy/configuration.rb +32 -2
- data/lib/phronomy/context/assembler.rb +6 -0
- data/lib/phronomy/context/compaction_context.rb +2 -0
- data/lib/phronomy/context/context_version_cache.rb +2 -0
- data/lib/phronomy/context/token_budget.rb +3 -0
- data/lib/phronomy/context/token_estimator.rb +9 -2
- data/lib/phronomy/context/trigger_context.rb +1 -0
- data/lib/phronomy/context/trim_context.rb +4 -0
- data/lib/phronomy/context.rb +0 -1
- data/lib/phronomy/embeddings/base.rb +5 -2
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +6 -2
- data/lib/phronomy/eval/comparison.rb +2 -0
- data/lib/phronomy/eval/dataset.rb +4 -0
- data/lib/phronomy/eval/metrics.rb +6 -0
- data/lib/phronomy/eval/runner.rb +2 -0
- data/lib/phronomy/eval/scorer/base.rb +1 -0
- data/lib/phronomy/eval/scorer/exact_match.rb +2 -0
- data/lib/phronomy/eval/scorer/includes_scorer.rb +2 -0
- data/lib/phronomy/eval/scorer/llm_judge.rb +2 -0
- data/lib/phronomy/event.rb +14 -0
- data/lib/phronomy/event_loop.rb +254 -0
- data/lib/phronomy/fsm_session.rb +201 -0
- data/lib/phronomy/generator_verifier.rb +24 -22
- data/lib/phronomy/guardrail/base.rb +3 -0
- data/lib/phronomy/guardrail.rb +0 -1
- data/lib/phronomy/knowledge_source/base.rb +6 -2
- data/lib/phronomy/knowledge_source/entity_knowledge.rb +7 -2
- data/lib/phronomy/knowledge_source/rag_knowledge.rb +8 -4
- data/lib/phronomy/knowledge_source/static_knowledge.rb +7 -2
- data/lib/phronomy/loader/base.rb +1 -0
- data/lib/phronomy/loader/csv_loader.rb +2 -0
- data/lib/phronomy/loader/markdown_loader.rb +2 -0
- data/lib/phronomy/loader/plain_text_loader.rb +1 -0
- data/lib/phronomy/output_parser/base.rb +1 -0
- data/lib/phronomy/output_parser/json_parser.rb +22 -3
- data/lib/phronomy/output_parser/structured_parser.rb +2 -0
- data/lib/phronomy/prompt_template.rb +5 -0
- data/lib/phronomy/runnable.rb +20 -3
- data/lib/phronomy/splitter/base.rb +2 -0
- data/lib/phronomy/splitter/fixed_size_splitter.rb +2 -0
- data/lib/phronomy/splitter/recursive_splitter.rb +2 -0
- data/lib/phronomy/state_store/base.rb +48 -0
- data/lib/phronomy/state_store/in_memory.rb +62 -0
- data/lib/phronomy/tool/agent_tool.rb +1 -0
- data/lib/phronomy/tool/base.rb +189 -27
- data/lib/phronomy/tool/mcp_tool.rb +68 -13
- data/lib/phronomy/tracing/base.rb +3 -0
- data/lib/phronomy/tracing/langfuse_tracer.rb +2 -0
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +2 -0
- data/lib/phronomy/vector_store/base.rb +33 -7
- data/lib/phronomy/vector_store/in_memory.rb +16 -7
- data/lib/phronomy/vector_store/pgvector.rb +40 -9
- data/lib/phronomy/vector_store/redis_search.rb +29 -8
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +175 -74
- data/lib/phronomy/workflow_context.rb +55 -5
- data/lib/phronomy/workflow_runner.rb +197 -114
- data/lib/phronomy.rb +74 -1
- data/scripts/api_snapshot.rb +91 -0
- data/scripts/check_api_annotations.rb +68 -0
- data/scripts/check_private_enforcement.rb +93 -0
- data/scripts/check_readme_runnable.rb +98 -0
- data/scripts/run_mutation.sh +46 -0
- metadata +50 -6
- data/lib/phronomy/context/builder.rb +0 -92
- data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +0 -100
- data/lib/phronomy/guardrail/builtin/prompt_injection_detector.rb +0 -67
- 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
|
|
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.
|
|
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-
|
|
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:
|