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
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Context
5
- # Assembles ordered context sections (system prompt, knowledge, conversation
6
- # history) within a given token budget.
7
- #
8
- # Usage:
9
- # builder = Phronomy::Context::Builder.new(budget: budget)
10
- # builder.add_system(instructions_text)
11
- # builder.add_knowledge(knowledge_text)
12
- # builder.add_messages(messages)
13
- # messages_to_send = builder.build
14
- #
15
- # Sections are added in priority order. When the budget is exceeded the
16
- # lower-priority tail of each section is truncated.
17
- class Builder
18
- # @param budget [Phronomy::Context::TokenBudget]
19
- def initialize(budget:)
20
- @budget = budget
21
- @system = nil
22
- @knowledge = []
23
- @messages = []
24
- end
25
-
26
- # Set the system instructions text (highest priority).
27
- # @param text [String]
28
- def add_system(text)
29
- @system = text.to_s
30
- self
31
- end
32
-
33
- # Append knowledge/RAG text (medium priority).
34
- # @param text [String]
35
- def add_knowledge(text)
36
- @knowledge << text.to_s
37
- self
38
- end
39
-
40
- # Set conversation messages (lowest priority — oldest are dropped first).
41
- # @param messages [Array] list of message-like objects with #role and #content
42
- def add_messages(messages)
43
- @messages = Array(messages)
44
- self
45
- end
46
-
47
- # Assemble the context respecting the token budget.
48
- #
49
- # Returns a hash with:
50
- # :system [String, nil] system prompt (instructions + knowledge)
51
- # :messages [Array] conversation messages that fit within the budget
52
- #
53
- # @return [Hash]
54
- def build
55
- used = 0
56
-
57
- # System prompt is always included (budget enforcement is informational only).
58
- system_text = [@system, *@knowledge].compact.join("\n\n")
59
- used += TokenEstimator.estimate(system_text)
60
-
61
- # Conversation messages — keep as many recent messages as fit.
62
- remaining = @budget.available(used: used)
63
- kept = fit_messages_to_budget(@messages, remaining)
64
-
65
- {
66
- system: system_text.empty? ? nil : system_text,
67
- messages: kept
68
- }
69
- end
70
-
71
- private
72
-
73
- # Greedily accumulate messages from newest to oldest, stop when budget runs out.
74
- def fit_messages_to_budget(messages, token_limit)
75
- return messages if token_limit <= 0 && messages.empty?
76
-
77
- accumulated = 0
78
- result = []
79
-
80
- messages.reverse_each do |msg|
81
- tokens = TokenEstimator.estimate(msg.content.to_s)
82
- break if accumulated + tokens > token_limit
83
-
84
- accumulated += tokens
85
- result.unshift(msg)
86
- end
87
-
88
- result
89
- end
90
- end
91
- end
92
- end
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Guardrail
5
- module Builtin
6
- # Input guardrail that detects common PII patterns in the input string.
7
- #
8
- # Four categories are supported and each can be individually toggled:
9
- #
10
- # - +:ssn+ — US Social Security Numbers (###-##-####)
11
- # - +:credit_card+ — Credit / debit card numbers
12
- # - +:email+ — E-mail addresses
13
- # - +:phone+ — Phone numbers
14
- #
15
- # All four categories are active by default.
16
- #
17
- # @example Default — all categories active:
18
- # agent.add_input_guardrail(Phronomy::Guardrail::Builtin::PIIPatternDetector.new)
19
- #
20
- # @example Only check for credit cards and email:
21
- # detector = Phronomy::Guardrail::Builtin::PIIPatternDetector.new(
22
- # detect: [:credit_card, :email]
23
- # )
24
- class PIIPatternDetector < InputGuardrail
25
- # Recognised PII categories and their detection patterns.
26
- PATTERNS = {
27
- # US Social Security Number: ###-##-#### (hyphens required).
28
- ssn: {
29
- pattern: /\b\d{3}-\d{2}-\d{4}\b/,
30
- label: "SSN"
31
- },
32
- # Credit / debit card: 16 digits, optionally separated by spaces or hyphens.
33
- # Matched candidates are additionally validated with the Luhn algorithm
34
- # to eliminate false positives from arbitrary 16-digit sequences.
35
- credit_card: {
36
- pattern: /\b(?:\d{4}[- ]?){3}\d{4}\b/,
37
- label: "credit card number",
38
- validate_luhn: true
39
- },
40
- # Email address (simplified RFC 5322).
41
- email: {
42
- pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
43
- label: "email address"
44
- },
45
- # Phone number: 3-digit area code, 3-4-digit exchange, 4-digit subscriber;
46
- # optional E.164 country-code prefix (e.g. +1, +44).
47
- phone: {
48
- pattern: /(?:\+\d{1,3}[.\- ]?)?\(?\d{3}\)?[.\- ]?\d{3,4}[.\- ]?\d{4}\b/,
49
- label: "phone number"
50
- }
51
- }.freeze
52
-
53
- ALL_CATEGORIES = PATTERNS.keys.freeze
54
-
55
- # @param detect [Array<Symbol>] categories to detect.
56
- # Defaults to all four: +:ssn+, +:credit_card+, +:email+, +:phone+.
57
- # @raise [ArgumentError] when an unknown category symbol is provided.
58
- def initialize(detect: ALL_CATEGORIES)
59
- unknown = Array(detect) - ALL_CATEGORIES
60
- raise ArgumentError, "Unknown PII categories: #{unknown.inspect}" if unknown.any?
61
-
62
- @active_patterns = Array(detect).map { |cat| PATTERNS.fetch(cat) }
63
- end
64
-
65
- # @param value [Object] the input to check
66
- # @raise [Phronomy::GuardrailError] when a PII pattern is matched,
67
- # with a message identifying the category.
68
- def check(value)
69
- text = value.to_s
70
- @active_patterns.each do |entry|
71
- detected = if entry[:validate_luhn]
72
- # Scan for all candidates then filter by Luhn check-digit validation.
73
- # This avoids false positives on arbitrary 16-digit strings (e.g. internal IDs).
74
- text.scan(entry[:pattern]).any? { |m| luhn_valid?(m.gsub(/[- ]/, "")) }
75
- else
76
- text.match?(entry[:pattern])
77
- end
78
- fail!("PII detected in input: #{entry[:label]}") if detected
79
- end
80
- end
81
-
82
- private
83
-
84
- # Returns true when +digits+ (a string of decimal digits) satisfies the
85
- # Luhn check-digit algorithm used by payment card networks.
86
- def luhn_valid?(digits)
87
- digits.chars.reverse.each_with_index.sum do |d, i|
88
- n = d.to_i
89
- if i.odd?
90
- doubled = n * 2
91
- (doubled > 9) ? (doubled - 9) : doubled
92
- else
93
- n
94
- end
95
- end % 10 == 0
96
- end
97
- end
98
- end
99
- end
100
- end
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Guardrail
5
- module Builtin
6
- # Input guardrail that detects common prompt injection attempts.
7
- #
8
- # Matches a built-in list of injection patterns (case-insensitive) and raises
9
- # {Phronomy::GuardrailError} when any pattern is found in the input string.
10
- # Additional patterns can be supplied via the +additional_patterns:+ argument.
11
- #
12
- # **Limitations**: the built-in patterns cover well-known English and Japanese
13
- # phrasings. Obfuscated, Base64-encoded, or novel injection phrasing may not
14
- # be detected. For higher-assurance use cases, combine this guardrail with an
15
- # LLM-based classifier.
16
- #
17
- # @example
18
- # agent.add_input_guardrail(
19
- # Phronomy::Guardrail::Builtin::PromptInjectionDetector.new
20
- # )
21
- #
22
- # # With extra patterns:
23
- # detector = Phronomy::Guardrail::Builtin::PromptInjectionDetector.new(
24
- # additional_patterns: [/do anything now/i]
25
- # )
26
- class PromptInjectionDetector < InputGuardrail
27
- # Default patterns that signal a prompt injection attempt.
28
- DEFAULT_PATTERNS = [
29
- # --- English patterns ---
30
- /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)/i,
31
- /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)/i,
32
- /forget\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?|prompts?)/i,
33
- /\bsystem\s*prompt\s*:/i,
34
- /\byou\s+are\s+now\s+(?:a|an)\b/i,
35
- /\bact\s+as\s+(?:a|an)\b/i,
36
- /\bpretend\s+(?:you\s+are|to\s+be)\b/i,
37
- /\bjailbreak\b/i,
38
- /\bdan\s*mode\b/i,
39
- /\bdev(?:eloper)?\s*mode\b/i,
40
- # --- Japanese patterns ---
41
- /以前の(指示|ルール|プロンプト)を無視/,
42
- /指示を無視して/,
43
- /ルールを無視して/,
44
- /あなたは今(から)?(?!助けて)/,
45
- /システムプロンプト/,
46
- /制約(を|から)無視/,
47
- /制限(を|から)解除/
48
- ].freeze
49
-
50
- # @param additional_patterns [Array<Regexp>] extra patterns to check in addition
51
- # to the built-in list.
52
- def initialize(additional_patterns: [])
53
- @patterns = DEFAULT_PATTERNS + Array(additional_patterns)
54
- end
55
-
56
- # @param value [Object] the input to check
57
- # @raise [Phronomy::GuardrailError] when an injection pattern is matched
58
- def check(value)
59
- text = value.to_s
60
- @patterns.each do |pattern|
61
- fail!("Potential prompt injection detected") if text.match?(pattern)
62
- end
63
- end
64
- end
65
- end
66
- end
67
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "builtin/prompt_injection_detector"
4
- require_relative "builtin/pii_pattern_detector"
5
-
6
- module Phronomy
7
- module Guardrail
8
- # Namespace for built-in guardrail implementations shipped with phronomy.
9
- #
10
- # Available classes:
11
- # - {Phronomy::Guardrail::Builtin::PromptInjectionDetector}
12
- # - {Phronomy::Guardrail::Builtin::PIIPatternDetector}
13
- module Builtin
14
- end
15
- end
16
- end