phronomy 0.2.2 → 0.4.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -30
  3. data/README.md +106 -122
  4. data/lib/phronomy/agent/base.rb +135 -57
  5. data/lib/phronomy/agent/checkpoint.rb +53 -0
  6. data/lib/phronomy/agent/orchestrator.rb +119 -0
  7. data/lib/phronomy/agent/react_agent.rb +18 -28
  8. data/lib/phronomy/agent/shared_state.rb +303 -0
  9. data/lib/phronomy/agent/suspend_signal.rb +35 -0
  10. data/lib/phronomy/agent/team_coordinator.rb +285 -0
  11. data/lib/phronomy/agent.rb +2 -1
  12. data/lib/phronomy/configuration.rb +0 -24
  13. data/lib/phronomy/generator_verifier.rb +250 -0
  14. data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +10 -27
  15. data/lib/phronomy/railtie.rb +0 -6
  16. data/lib/phronomy/ruby_llm_patches.rb +20 -0
  17. data/lib/phronomy/tool/mcp_tool.rb +23 -26
  18. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -6
  19. data/lib/phronomy/vector_store/redis_search.rb +4 -4
  20. data/lib/phronomy/version.rb +1 -1
  21. data/lib/phronomy/workflow.rb +4 -7
  22. data/lib/phronomy/workflow_runner.rb +42 -30
  23. data/lib/phronomy.rb +18 -0
  24. data/scripts/check_readme_ruby.rb +38 -0
  25. metadata +12 -38
  26. data/docs/trustworthy_ai_enhancements.md +0 -332
  27. data/lib/phronomy/active_record/acts_as.rb +0 -48
  28. data/lib/phronomy/active_record/checkpoint.rb +0 -20
  29. data/lib/phronomy/active_record/extensions.rb +0 -14
  30. data/lib/phronomy/active_record/message.rb +0 -20
  31. data/lib/phronomy/actor.rb +0 -68
  32. data/lib/phronomy/memory/compression/base.rb +0 -37
  33. data/lib/phronomy/memory/compression/summary.rb +0 -107
  34. data/lib/phronomy/memory/compression/tool_output_pruner.rb +0 -67
  35. data/lib/phronomy/memory/compression.rb +0 -11
  36. data/lib/phronomy/memory/conversation_manager.rb +0 -213
  37. data/lib/phronomy/memory/retrieval/base.rb +0 -22
  38. data/lib/phronomy/memory/retrieval/composite.rb +0 -76
  39. data/lib/phronomy/memory/retrieval/recent.rb +0 -35
  40. data/lib/phronomy/memory/retrieval/semantic.rb +0 -114
  41. data/lib/phronomy/memory/retrieval.rb +0 -12
  42. data/lib/phronomy/memory/storage/active_record.rb +0 -248
  43. data/lib/phronomy/memory/storage/base.rb +0 -155
  44. data/lib/phronomy/memory/storage/in_memory.rb +0 -152
  45. data/lib/phronomy/memory/storage.rb +0 -11
  46. data/lib/phronomy/memory.rb +0 -21
  47. data/lib/phronomy/rails/agent_job.rb +0 -75
  48. data/lib/phronomy/state_store/active_record.rb +0 -76
  49. data/lib/phronomy/state_store/base.rb +0 -112
  50. data/lib/phronomy/state_store/encryptor/active_support.rb +0 -49
  51. data/lib/phronomy/state_store/encryptor/base.rb +0 -34
  52. data/lib/phronomy/state_store/encryptor.rb +0 -16
  53. data/lib/phronomy/state_store/file.rb +0 -85
  54. data/lib/phronomy/state_store/in_memory.rb +0 -53
  55. data/lib/phronomy/state_store/redis.rb +0 -70
  56. data/lib/phronomy/state_store.rb +0 -9
  57. data/lib/phronomy/thread_actor_registry.rb +0 -85
  58. data/lib/phronomy/trust_pipeline.rb +0 -264
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Implements the Generator-Verifier multi-agent coordination pattern
5
+ # (Anthropic blog, Pattern 1): a generator agent produces an
6
+ # answer while a verifier agent evaluates its quality.
7
+ #
8
+ # @see https://claude.com/blog/multi-agent-coordination-patterns
9
+ #
10
+ # All prompt construction and result parsing are provided by the caller,
11
+ # giving full control over the LLM dialogue.
12
+ # The generator and verifier agents are configurable, and the pipeline
13
+ # retries until confidence passes the threshold or max iterations are reached.
14
+ #
15
+ # @example Basic usage with custom prompt builders
16
+ # pipeline = Phronomy::GeneratorVerifier.new(
17
+ # draft_agent: MyDraftAgent,
18
+ # review_agent: MyReviewAgent,
19
+ # draft_prompt_builder: ->(input, feedback) { "Question: #{input}" },
20
+ # review_prompt_builder: ->(input, draft, citations) { "Review: #{draft}" }
21
+ # )
22
+ # result = pipeline.invoke("What is the refund policy?")
23
+ # puts result.output # the final answer string
24
+ # puts result.trusted? # true when confidence >= threshold
25
+ #
26
+ # @example Custom result parsers
27
+ # pipeline = Phronomy::GeneratorVerifier.new(
28
+ # ...,
29
+ # draft_result_parser: ->(text) { my_parse_draft(text) },
30
+ # review_result_parser: ->(text) { my_parse_review(text) }
31
+ # )
32
+ #
33
+ # @example Raising on low confidence
34
+ # pipeline = Phronomy::GeneratorVerifier.new(
35
+ # ...,
36
+ # raise_if_untrusted: true
37
+ # )
38
+ # begin
39
+ # result = pipeline.invoke("question")
40
+ # rescue Phronomy::LowConfidenceError => e
41
+ # puts "Untrusted: #{e.result.confidence}"
42
+ # end
43
+ class GeneratorVerifier
44
+ # Default confidence threshold for trusting an answer.
45
+ DEFAULT_CONFIDENCE_THRESHOLD = 0.7
46
+
47
+ # Default maximum draft-review cycles before returning best effort.
48
+ DEFAULT_MAX_ITERATIONS = 3
49
+
50
+ # Immutable value object returned by {GeneratorVerifier#invoke}.
51
+ #
52
+ # @!attribute [r] output
53
+ # @return [String] the final answer text
54
+ # @!attribute [r] confidence
55
+ # @return [Float] combined confidence score (0.0–1.0)
56
+ # @!attribute [r] citations
57
+ # @return [Array<Hash>] [{source:, excerpt:}, ...]
58
+ #
59
+ # **WARNING**: These citations are extracted from the LLM's own response
60
+ # and are **not** verified against any external knowledge base or URL.
61
+ # Do not treat them as authoritative without independent verification.
62
+ # @!attribute [r] iterations
63
+ # @return [Integer] number of draft-review cycles executed
64
+ # @!attribute [r] review_notes
65
+ # @return [Array<String>] reviewer feedback for each cycle
66
+ # @!attribute [r] trusted
67
+ # @return [Boolean] true when confidence >= threshold
68
+ Result = Struct.new(
69
+ :output, :confidence, :citations, :iterations, :review_notes, :trusted,
70
+ keyword_init: true
71
+ ) do
72
+ # @return [Boolean] true when confidence >= threshold
73
+ alias_method :trusted?, :trusted
74
+ end
75
+
76
+ # Internal graph state — not part of the public API.
77
+ # @private
78
+ class PipelineState
79
+ include Phronomy::WorkflowContext
80
+
81
+ field :input, type: :replace, default: -> { "" }
82
+ field :draft, type: :replace, default: -> {}
83
+ field :self_score, type: :replace, default: -> { 0.0 }
84
+ field :review_score, type: :replace, default: -> { 0.0 }
85
+ field :citations, type: :replace, default: -> { [] }
86
+ field :review_notes, type: :append, default: -> { [] }
87
+ field :iteration, type: :replace, default: -> { 0 }
88
+ field :approved, type: :replace, default: -> { false }
89
+ field :output, type: :replace, default: -> {}
90
+ end
91
+
92
+ private_constant :PipelineState
93
+
94
+ # @param draft_agent [Class] subclass of Phronomy::Agent::Base
95
+ # used to generate answer drafts
96
+ # @param review_agent [Class] subclass of Phronomy::Agent::Base
97
+ # used to evaluate each draft
98
+ # @param draft_prompt_builder [#call] +call(input, feedback)+ → String
99
+ # prompt for the generator. +feedback+ is nil on the first iteration and
100
+ # contains the reviewer's feedback string on subsequent iterations.
101
+ # @param review_prompt_builder [#call] +call(input, draft, citations)+ → String
102
+ # prompt for the verifier. +citations+ is an Array of Hashes.
103
+ # @param draft_result_parser [#call, nil] +call(text)+ → Hash with
104
+ # +:answer+, +:confidence+, and +:citations+ keys. Defaults to JSON parsing
105
+ # with a safe fallback when the response cannot be parsed.
106
+ # @param review_result_parser [#call, nil] +call(text)+ → Hash with
107
+ # +:approved+, +:score+, and +:feedback+ keys. Defaults to JSON parsing
108
+ # with a safe fallback.
109
+ # @param confidence_threshold [Float] minimum combined confidence to
110
+ # trust an answer (default: 0.7)
111
+ # @param max_iterations [Integer] maximum draft-review cycles
112
+ # before returning the best-effort answer (default: 3)
113
+ # @param raise_if_untrusted [Boolean] when +true+, raises
114
+ # {Phronomy::LowConfidenceError} if the final result does not meet the
115
+ # confidence threshold (default: false)
116
+ def initialize(
117
+ draft_agent:,
118
+ review_agent:,
119
+ draft_prompt_builder:,
120
+ review_prompt_builder:,
121
+ draft_result_parser: nil,
122
+ review_result_parser: nil,
123
+ confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
124
+ max_iterations: DEFAULT_MAX_ITERATIONS,
125
+ raise_if_untrusted: false
126
+ )
127
+ @draft_agent_class = draft_agent
128
+ @review_agent_class = review_agent
129
+ @draft_prompt_builder = draft_prompt_builder
130
+ @review_prompt_builder = review_prompt_builder
131
+ @draft_result_parser = draft_result_parser || method(:default_parse_draft)
132
+ @review_result_parser = review_result_parser || method(:default_parse_review)
133
+ @threshold = confidence_threshold.to_f
134
+ @max_iterations = max_iterations.to_i
135
+ @raise_if_untrusted = raise_if_untrusted
136
+ @compiled_graph = nil
137
+ end
138
+
139
+ # Run the generator-verifier pipeline.
140
+ #
141
+ # @param input [String] the user question or task description
142
+ # @param config [Hash] forwarded to the underlying agents (e.g. thread_id)
143
+ # @return [Result]
144
+ # @raise [Phronomy::LowConfidenceError] when +raise_if_untrusted:+ is +true+
145
+ # and the result does not meet the confidence threshold
146
+ def invoke(input, config: {})
147
+ app = compiled_graph
148
+ state = app.invoke({input: input}, config: config)
149
+ confidence = combined_confidence(state)
150
+ trusted = confidence >= @threshold
151
+ result = Result.new(
152
+ output: state.output || state.draft.to_s,
153
+ confidence: confidence,
154
+ citations: state.citations,
155
+ iterations: state.iteration,
156
+ review_notes: state.review_notes,
157
+ trusted: trusted
158
+ )
159
+ raise LowConfidenceError.new(result) if @raise_if_untrusted && !trusted
160
+ result
161
+ end
162
+
163
+ private
164
+
165
+ def combined_confidence(state)
166
+ [(state.self_score || 0.0).to_f, (state.review_score || 0.0).to_f].min
167
+ end
168
+
169
+ def compiled_graph
170
+ @compiled_graph ||= build_workflow
171
+ end
172
+
173
+ def build_workflow
174
+ draft_agent = @draft_agent_class.new
175
+ review_agent = @review_agent_class.new
176
+ threshold = @threshold
177
+ max_iter = @max_iterations
178
+ dpb = @draft_prompt_builder
179
+ rpb = @review_prompt_builder
180
+ drp = @draft_result_parser
181
+ rrp = @review_result_parser
182
+ pipeline = self
183
+
184
+ Phronomy::Workflow.define(PipelineState) do
185
+ initial :draft
186
+
187
+ state :draft, action: ->(state) {
188
+ feedback = state.review_notes.last
189
+ prompt = dpb.call(state.input, feedback)
190
+ result = draft_agent.invoke(prompt)
191
+ parsed = drp.call(result[:output])
192
+ state.merge(
193
+ draft: parsed[:answer].to_s,
194
+ self_score: pipeline.__send__(:clamp, parsed[:confidence]),
195
+ citations: pipeline.__send__(:normalize_citations, parsed[:citations]),
196
+ iteration: state.iteration + 1
197
+ )
198
+ }
199
+
200
+ state :review, action: ->(state) {
201
+ prompt = rpb.call(state.input, state.draft, state.citations)
202
+ result = review_agent.invoke(prompt)
203
+ parsed = rrp.call(result[:output])
204
+ state.merge(
205
+ review_score: pipeline.__send__(:clamp, parsed[:score]),
206
+ approved: parsed[:approved] == true,
207
+ review_notes: parsed[:feedback].to_s
208
+ )
209
+ }
210
+
211
+ state :finalize, action: ->(state) { state.merge(output: state.draft) }
212
+
213
+ after :draft, to: :review
214
+ after :finalize, to: :__finish__
215
+
216
+ event :route_review, from: :review,
217
+ guard: ->(state) {
218
+ confidence = [state.self_score || 0.0, state.review_score || 0.0].min
219
+ (confidence >= threshold && state.approved) || state.iteration >= max_iter
220
+ },
221
+ to: :finalize
222
+ event :route_review, from: :review, to: :draft
223
+ end
224
+ end
225
+
226
+ def default_parse_draft(text)
227
+ json_parser.parse(text)
228
+ rescue Phronomy::ParseError
229
+ {answer: text.to_s, confidence: 0.0, citations: []}
230
+ end
231
+
232
+ def default_parse_review(text)
233
+ json_parser.parse(text)
234
+ rescue Phronomy::ParseError
235
+ {approved: false, score: 0.0, feedback: "Review output could not be parsed: #{text}"}
236
+ end
237
+
238
+ def json_parser
239
+ @json_parser ||= Phronomy::OutputParser::JsonParser.new
240
+ end
241
+
242
+ def clamp(val)
243
+ val.to_f.clamp(0.0, 1.0)
244
+ end
245
+
246
+ def normalize_citations(raw)
247
+ Array(raw).filter_map { |c| c.is_a?(Hash) ? c.transform_keys(&:to_sym) : nil }
248
+ end
249
+ end
250
+ end
@@ -7,10 +7,10 @@ module Phronomy
7
7
  #
8
8
  # Four categories are supported and each can be individually toggled:
9
9
  #
10
- # - +:my_number+ Japanese My Number (12-digit national ID)
10
+ # - +:ssn+ US Social Security Numbers (###-##-####)
11
11
  # - +:credit_card+ — Credit / debit card numbers
12
12
  # - +:email+ — E-mail addresses
13
- # - +:phone+ — Japanese domestic phone numbers
13
+ # - +:phone+ — Phone numbers
14
14
  #
15
15
  # All four categories are active by default.
16
16
  #
@@ -24,13 +24,10 @@ module Phronomy
24
24
  class PIIPatternDetector < InputGuardrail
25
25
  # Recognised PII categories and their detection patterns.
26
26
  PATTERNS = {
27
- # Japanese My Number: 12 consecutive or grouped digits (4-4-4).
28
- # Matched candidates are additionally validated with the official check-digit
29
- # algorithm (JIS X 0076) to eliminate false positives from arbitrary 12-digit strings.
30
- my_number: {
31
- pattern: /(?<!\d)(?<!\d[- ])\d{4}[- ]?\d{4}[- ]?\d{4}(?![- ]?\d)/,
32
- label: "My Number",
33
- validate_my_number: true
27
+ # US Social Security Number: ###-##-#### (hyphens required).
28
+ ssn: {
29
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/,
30
+ label: "SSN"
34
31
  },
35
32
  # Credit / debit card: 16 digits, optionally separated by spaces or hyphens.
36
33
  # Matched candidates are additionally validated with the Luhn algorithm
@@ -45,9 +42,10 @@ module Phronomy
45
42
  pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
46
43
  label: "email address"
47
44
  },
48
- # Japanese phone number: starts with 0, groups of 2-5 / 1-4 / 4 digits.
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).
49
47
  phone: {
50
- pattern: /\b0\d{1,4}[- ]?\d{1,4}[- ]?\d{4}\b/,
48
+ pattern: /(?:\+\d{1,3}[.\- ]?)?\(?\d{3}\)?[.\- ]?\d{3,4}[.\- ]?\d{4}\b/,
51
49
  label: "phone number"
52
50
  }
53
51
  }.freeze
@@ -55,7 +53,7 @@ module Phronomy
55
53
  ALL_CATEGORIES = PATTERNS.keys.freeze
56
54
 
57
55
  # @param detect [Array<Symbol>] categories to detect.
58
- # Defaults to all four: +:my_number+, +:credit_card+, +:email+, +:phone+.
56
+ # Defaults to all four: +:ssn+, +:credit_card+, +:email+, +:phone+.
59
57
  # @raise [ArgumentError] when an unknown category symbol is provided.
60
58
  def initialize(detect: ALL_CATEGORIES)
61
59
  unknown = Array(detect) - ALL_CATEGORIES
@@ -74,10 +72,6 @@ module Phronomy
74
72
  # Scan for all candidates then filter by Luhn check-digit validation.
75
73
  # This avoids false positives on arbitrary 16-digit strings (e.g. internal IDs).
76
74
  text.scan(entry[:pattern]).any? { |m| luhn_valid?(m.gsub(/[- ]/, "")) }
77
- elsif entry[:validate_my_number]
78
- # Scan for all candidates then apply the JIS X 0076 check-digit algorithm.
79
- # This avoids false positives on arbitrary 12-digit strings.
80
- text.scan(entry[:pattern]).any? { |m| my_number_valid?(m.gsub(/[- ]/, "")) }
81
75
  else
82
76
  text.match?(entry[:pattern])
83
77
  end
@@ -87,17 +81,6 @@ module Phronomy
87
81
 
88
82
  private
89
83
 
90
- # Returns true when +digits+ (a 12-character string of decimal digits) satisfies
91
- # the Japanese My Number check-digit algorithm defined in JIS X 0076.
92
- # The check digit is the 12th digit.
93
- def my_number_valid?(digits)
94
- weights = [6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
95
- total = weights.each_with_index.sum { |w, i| w * digits[i].to_i }
96
- remainder = total % 11
97
- check = (remainder <= 1) ? 0 : 11 - remainder
98
- check == digits[11].to_i
99
- end
100
-
101
84
  # Returns true when +digits+ (a string of decimal digits) satisfies the
102
85
  # Luhn check-digit algorithm used by payment card networks.
103
86
  def luhn_valid?(digits)
@@ -30,16 +30,10 @@ module Phronomy
30
30
 
31
31
  # Loads Phronomy::Rails::AgentJob when both ActionCable and ActiveJob are present.
32
32
  initializer "phronomy.agent_job" do
33
- if defined?(::ActionCable) && defined?(::ActiveJob)
34
- require "phronomy/rails/agent_job"
35
- end
36
33
  end
37
34
 
38
35
  # Loads Phronomy ActiveRecord extensions when ActiveRecord is available.
39
36
  initializer "phronomy.active_record", after: "active_record.initialize_database" do
40
- ActiveSupport.on_load(:active_record) do
41
- require "phronomy/active_record/extensions" if defined?(::ActiveRecord)
42
- end
43
37
  end
44
38
  end
45
39
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Patches for upstream ruby_llm bugs that have not yet been released.
4
+ # Remove each patch once the fix is available in a published gem version.
5
+
6
+ module RubyLLM
7
+ module Streaming
8
+ private
9
+
10
+ # Upstream ruby_llm <= 1.15.0 assumes the SSE error chunk always has two
11
+ # lines ("event: error\ndata: {...}") and uses a fixed index [1], which
12
+ # raises NoMethodError when some providers (e.g. Qwen) return a single-line
13
+ # chunk ("data: {...}"). This patch finds the data line by content instead.
14
+ def handle_error_chunk(chunk, env)
15
+ data_line = chunk.split("\n").find { |l| l.start_with?("data: ") } || chunk.split("\n")[0]
16
+ error_data = data_line.delete_prefix("data: ")
17
+ parse_error_from_json(error_data, env, "Failed to parse error chunk")
18
+ end
19
+ end
20
+ end
@@ -11,10 +11,11 @@ module Phronomy
11
11
  # A Phronomy::Tool::Base subclass that wraps a tool exposed by an external
12
12
  # MCP (Model Context Protocol) server.
13
13
  #
14
- # Currently supports the **stdio** transport only: the MCP server is launched
15
- # as a child process and communicates via newline-delimited JSON-RPC on stdin/stdout.
16
- #
17
- # HTTP/SSE transport support can be added later by subclassing Transport.
14
+ # Supports two transport schemes:
15
+ # - <b>"stdio://\<command\>"</b> — spawns a child process that communicates via
16
+ # newline-delimited JSON-RPC on stdin/stdout.
17
+ # - <b>"http://\<url\>"</b> / <b>"https://\<url\>"</b> connects to a running
18
+ # HTTP/SSE MCP server using +net/http+.
18
19
  #
19
20
  # @example
20
21
  # web_search = Phronomy::Tool::McpTool.from_server(
@@ -31,6 +32,7 @@ module Phronomy
31
32
  # @param server_uri [String] URI of the MCP server.
32
33
  # Supported schemes:
33
34
  # - "stdio://<command>" — spawn a child process
35
+ # - "http://<url>" / "https://<url>" — connect to an HTTP/SSE server
34
36
  # @param tool_name [String] the tool name as registered in the MCP server
35
37
  # @return [McpTool] a configured subclass instance ready for use with an Agent
36
38
  def from_server(server_uri, tool_name:)
@@ -87,7 +89,6 @@ module Phronomy
87
89
  # Split the command string into an argv array so that Open3 executes
88
90
  # it directly without going through the shell, preventing injection.
89
91
  @command = Shellwords.split(command)
90
- @actor = Phronomy::Actor.new
91
92
  @stdin = nil
92
93
  @stdout = nil
93
94
  @stderr = nil
@@ -97,19 +98,17 @@ module Phronomy
97
98
 
98
99
  # Shut down the child process and close its IO streams.
99
100
  def close
100
- stderr_thread, wait_thr = @actor.call do
101
- @stdin&.close
102
- @stdout&.close
103
- @stderr&.close
104
- @stdin = nil
105
- @stdout = nil
106
- @stderr = nil
107
- t = [@stderr_thread, @wait_thr]
108
- @stderr_thread = nil
109
- @wait_thr = nil
110
- t
111
- end
112
- # Join outside the Actor to avoid blocking the Actor thread on slow joins.
101
+ @stdin&.close
102
+ @stdout&.close
103
+ @stderr&.close
104
+ @stdin = nil
105
+ @stdout = nil
106
+ @stderr = nil
107
+ stderr_thread = @stderr_thread
108
+ wait_thr = @wait_thr
109
+ @stderr_thread = nil
110
+ @wait_thr = nil
111
+ # Join outside the lock to avoid blocking on slow joins.
113
112
  stderr_thread&.join(1)
114
113
  wait_thr&.join(5)
115
114
  end
@@ -168,14 +167,12 @@ module Phronomy
168
167
  end
169
168
 
170
169
  def rpc_call(method, params)
171
- @actor.call do
172
- ensure_started!
173
- payload = JSON.generate(jsonrpc: "2.0", id: SecureRandom.uuid, method: method, params: params)
174
- @stdin.puts(payload)
175
- raw = @stdout.gets
176
- raise Phronomy::ToolError, "MCP server closed the connection unexpectedly" if raw.nil?
177
- JSON.parse(raw)
178
- end
170
+ ensure_started!
171
+ payload = JSON.generate(jsonrpc: "2.0", id: SecureRandom.uuid, method: method, params: params)
172
+ @stdin.puts(payload)
173
+ raw = @stdout.gets
174
+ raise Phronomy::ToolError, "MCP server closed the connection unexpectedly" if raw.nil?
175
+ JSON.parse(raw)
179
176
  end
180
177
 
181
178
  def parse_schema_params(properties)
@@ -36,7 +36,6 @@ module Phronomy
36
36
  @secret_key = secret_key
37
37
  @host = host.chomp("/")
38
38
  @http = nil
39
- @actor = Phronomy::Actor.new
40
39
  end
41
40
 
42
41
  # Returns a plain Hash that records the span start state.
@@ -90,13 +89,11 @@ module Phronomy
90
89
  req["Authorization"] = "Basic #{Base64.strict_encode64("#{@public_key}:#{@secret_key}")}"
91
90
  req.body = JSON.generate({batch: events})
92
91
 
93
- @actor.call do
94
- @http ||= build_http(uri)
95
- @http.request(req)
96
- end
92
+ @http ||= build_http(uri)
93
+ @http.request(req)
97
94
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
98
95
  # Connection was reset; drop the cached connection and warn.
99
- @actor.call { @http = nil }
96
+ @http = nil
100
97
  warn "[Phronomy::LangfuseTracer] Ingestion failed: #{e.class}: #{e.message}"
101
98
  nil
102
99
  rescue => e
@@ -38,7 +38,7 @@ module Phronomy
38
38
  @index_name = index_name
39
39
  @dimension = dimension
40
40
  @index_created = false
41
- @actor = Phronomy::Actor.new
41
+ @mutex = Mutex.new
42
42
  end
43
43
 
44
44
  # @param id [String]
@@ -80,7 +80,7 @@ module Phronomy
80
80
  end
81
81
 
82
82
  def clear
83
- @actor.call do
83
+ @mutex.synchronize do
84
84
  begin
85
85
  @redis.call("FT.DROPINDEX", @index_name, "DD")
86
86
  rescue => e
@@ -94,8 +94,8 @@ module Phronomy
94
94
  private
95
95
 
96
96
  def ensure_index!(dim)
97
- @actor.call do
98
- next if @index_created
97
+ @mutex.synchronize do
98
+ return if @index_created
99
99
 
100
100
  @dimension ||= dim
101
101
  begin
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -53,11 +53,10 @@ module Phronomy
53
53
 
54
54
  # Defines a new Workflow.
55
55
  # @param context_class [Class] class that includes Phronomy::WorkflowContext
56
- # @param state_store [Object, nil] optional state store override (passed to WorkflowRunner)
57
56
  # @yield block evaluated in DSL context
58
57
  # @return [Phronomy::Workflow] compiled and ready-to-run workflow instance
59
- def self.define(context_class, state_store: nil, &block)
60
- builder = Builder.new(context_class, state_store: state_store)
58
+ def self.define(context_class, &block)
59
+ builder = Builder.new(context_class)
61
60
  builder.instance_eval(&block)
62
61
  builder.build
63
62
  end
@@ -110,9 +109,8 @@ module Phronomy
110
109
  class Builder
111
110
  FINISH = Phronomy::WorkflowRunner::FINISH
112
111
 
113
- def initialize(context_class, state_store: nil)
112
+ def initialize(context_class)
114
113
  @context_class = context_class
115
- @state_store = state_store
116
114
  @initial = nil
117
115
  # { node_name => callable }
118
116
  @states = {}
@@ -210,8 +208,7 @@ module Phronomy
210
208
  route_transitions: route_transitions,
211
209
  external_events: external_events,
212
210
  entry_point: @initial || nodes.keys.first,
213
- wait_state_names: @wait_state_names,
214
- state_store: @state_store
211
+ wait_state_names: @wait_state_names
215
212
  )
216
213
 
217
214
  Workflow.new(runner)