phronomy 0.5.0 → 0.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 138e6b7d6b59f34f827e39a43b86c6f30ea0dd80e936d11e326febad4d3217b0
4
- data.tar.gz: fada502e034850a3162a488cb02fc195364fc93e72398e858a79058c005c2ad3
3
+ metadata.gz: 8ca63f10e2d505005a6011a8755135e0dea1c3d8e8013ae3b2ea1d3b5ecf13d3
4
+ data.tar.gz: 420ec691725b6450a0430cdce90372c13da4e73d7013a630b2aa33a7b72e496d
5
5
  SHA512:
6
- metadata.gz: 55526d56e69e328f9de38e75da98a9a1e0d206997f3463a18aa0481f18d978896f02567a0fefcb6ec4fe2a5f030d3829dde59c479305f6e3ce9d825b06222ce8
7
- data.tar.gz: f58b275260866c5a7784c32c9846c9058cab815d6d294b92041dcf29525bbf76e1683c1151991c092eace65e5e55e41ad5390d6f6106f9306aa053bf42c5c0a8
6
+ metadata.gz: 300b155e482fe0b0015bba5d3985c8d37f2599828d9d87763cc5d9925c3bf399cbee46a658d48a79e152d9b410505609e35a89d3b02db632d51b85280938dc8c
7
+ data.tar.gz: 88184d595d04eb1ed1be8c6ec145476a5e1f7e6e02e52658f9d80ad3893a29fbcfc377c736b7c0de10deb14e6105bd9594d6b950bd2fb3afda800e770a2219c4
data/CHANGELOG.md CHANGED
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.5.1] - 2026-05-21
11
+
12
+ ### Bug Fixes
13
+
14
+ - **Remove broken Rails generator and Railtie** (#85): The generator template
15
+ referenced `Phronomy::ActiveRecord::ActsAs` which no longer exists, causing
16
+ `rails generate phronomy:install` to produce broken model files. Removed
17
+ `lib/generators/`, `lib/phronomy/railtie.rb`, and all references in
18
+ `lib/phronomy.rb`.
19
+
20
+ - **Fix thread-safety of `StdioTransport#rpc_call`** (#86): Concurrent calls
21
+ to the same `McpTool` instance could interleave JSON-RPC writes and reads,
22
+ corrupting request/response pairing. A `Mutex` is now held around each
23
+ write+read cycle. Also adds the missing `require "securerandom"`.
24
+
25
+ ### Documentation
26
+
27
+ - **README corrections** (#87): Remove stale Rails generator installation
28
+ instructions. Clarify that `TeamCoordinator` worker state is local to a
29
+ single `invoke` call (not persistent across calls). Annotate app-level
30
+ examples (`09_rails_chat`, `15_rails_secure_chat`, `18_rails_agent_job`,
31
+ `19_trust_pipeline`) as requiring external infrastructure. Add scope note
32
+ to `Agent::Orchestrator` section.
33
+
34
+ ### Maintenance
35
+
36
+ - **Add version guard to `ruby_llm_patches.rb`** (#88): The monkey-patch for
37
+ the upstream `handle_error_chunk` bug (ruby_llm <= 1.15.0) is now
38
+ gated behind a `Gem::Version` check so upgrading ruby_llm will
39
+ automatically disable the override.
40
+
41
+ ---
42
+
10
43
  ## [0.5.0] - 2026-05-20
11
44
 
12
45
  ### Breaking Changes
data/README.md CHANGED
@@ -20,7 +20,7 @@ It provides composable building blocks — Workflows, Agents, Tools, Guardrails,
20
20
  | **Multi-agent** — Agent-as-Tool pattern and hub-and-spoke handoff routing | Beta |
21
21
  | **GeneratorVerifier** — Generator-Verifier loop with injectable prompt builders/parsers | Beta |
22
22
  | **Agent::Orchestrator** — Parallel subagent dispatch, fan-out, and `subagent` DSL | Beta |
23
- | **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + persistent worker pool with task queue | Beta |
23
+ | **Agent::TeamCoordinator** — Agent teams pattern: LLM coordinator + stateful worker pool with task queue (worker-local message history per run) | Beta |
24
24
  | **Agent::SharedState** — Shared state pattern: peer agents collaborate via a shared KnowledgeStore; `member` DSL with per-agent instructions and `coordination` team protocol | Experimental |
25
25
  | **Guardrails** — Input/output validation; built-in PII and prompt-injection detectors | Beta |
26
26
  | **Output Parser** — JSON and Struct-mapped parsers for structured LLM responses | Stable |
@@ -42,14 +42,6 @@ Then run:
42
42
  bundle install
43
43
  ```
44
44
 
45
- For Rails apps, run the install generator after bundling:
46
-
47
- ```bash
48
- rails generate phronomy:install
49
- ```
50
-
51
- This creates a configuration initializer.
52
-
53
45
  ## Quick Start
54
46
 
55
47
  ### Agent — ReAct tool-calling agent
@@ -279,6 +271,11 @@ end
279
271
 
280
272
  ### Agent::Orchestrator — Parallel subagent dispatch
281
273
 
274
+ > **Note:** `dispatch_parallel` and `fan_out` use plain Ruby threads and are
275
+ > intended for small-scale fan-out (a handful of subagents). For large-scale
276
+ > parallel dispatch, manage concurrency (thread pools, rate limiting) at the
277
+ > application level.
278
+
282
279
  ```ruby
283
280
  class ResearchOrchestrator < Phronomy::Agent::Orchestrator
284
281
  model "gpt-4o"
@@ -491,15 +488,21 @@ bundle exec ruby NN_example_name/run.rb
491
488
  | 06 | `06_guardrails/` | Input/output guardrails |
492
489
  | 07 | `07_tracing/` | Custom observability with Langfuse tracer |
493
490
  | 08 | `08_mcp_tool/` | MCP tool integration |
494
- | 09 | `09_rails_chat/` | Rails chat app with ActionCable streaming |
495
491
  | 10 | `10_context_management/` | Token budget and context pruning |
496
492
  | 11 | `11_agent_streaming/` | Streaming agent responses |
497
493
  | 12 | `12_prompt_template/` | Advanced prompt templates |
498
494
  | 13 | `13_mcp_http_tool/` | HTTP-based MCP tool server |
499
495
  | 14 | `14_code_review/` | Automated code review agent |
500
- | 15 | `15_rails_secure_chat/` | Rails chat with PII guardrails |
501
496
  | 16 | `16_before_completion_hook/` | Global/class/instance before_completion hooks |
502
497
  | 17 | `17_multi_agent_handoff/` | Hub-and-spoke agent routing via Runner |
498
+
499
+ The following examples are **app-level demos** (Rails apps or advanced pipelines)
500
+ that require additional infrastructure (a running Rails server, database, etc.):
501
+
502
+ | # | Directory | What it demonstrates |
503
+ |---|-----------|----------------------|
504
+ | 09 | `09_rails_chat/` | Rails chat app with ActionCable streaming |
505
+ | 15 | `15_rails_secure_chat/` | Rails chat with PII guardrails |
503
506
  | 18 | `18_rails_agent_job/` | Rails app with AgentJob + ActionCable streaming |
504
507
  | 19 | `19_trust_pipeline/` | Generator-Verifier pattern with citation tracking, self-review loop and confidence gate |
505
508
 
@@ -3,18 +3,22 @@
3
3
  # Patches for upstream ruby_llm bugs that have not yet been released.
4
4
  # Remove each patch once the fix is available in a published gem version.
5
5
 
6
- module RubyLLM
7
- module Streaming
8
- private
6
+ # Guard: apply monkey-patches only to affected ruby_llm versions so that
7
+ # upgrading the gem does not silently keep dead overrides in place.
8
+ if Gem::Version.new(RubyLLM::VERSION) <= Gem::Version.new("1.15.0")
9
+ module RubyLLM
10
+ module Streaming
11
+ private
9
12
 
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")
13
+ # Upstream ruby_llm <= 1.15.0 assumes the SSE error chunk always has two
14
+ # lines ("event: error\ndata: {...}") and uses a fixed index [1], which
15
+ # raises NoMethodError when some providers (e.g. Qwen) return a single-line
16
+ # chunk ("data: {...}"). This patch finds the data line by content instead.
17
+ def handle_error_chunk(chunk, env)
18
+ data_line = chunk.split("\n").find { |l| l.start_with?("data: ") } || chunk.split("\n")[0]
19
+ error_data = data_line.delete_prefix("data: ")
20
+ parse_error_from_json(error_data, env, "Failed to parse error chunk")
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "net/http"
5
5
  require "open3"
6
+ require "securerandom"
6
7
  require "shellwords"
7
8
  require "uri"
8
9
 
@@ -36,9 +37,13 @@ module Phronomy
36
37
  # @param tool_name [String] the tool name as registered in the MCP server
37
38
  # @return [McpTool] a configured subclass instance ready for use with an Agent
38
39
  def from_server(server_uri, tool_name:)
40
+ # Use a short-lived transport only to query the tool definition,
41
+ # then close it. Each McpTool instance creates its own transport
42
+ # so that concurrent callers never share IO streams.
39
43
  transport = build_transport(server_uri)
40
44
  tool_def = transport.fetch_tool(tool_name)
41
- build_tool_class(tool_name, tool_def, transport).new
45
+ transport.close
46
+ build_tool_class(tool_name, server_uri, tool_def).new
42
47
  end
43
48
 
44
49
  private
@@ -55,10 +60,10 @@ module Phronomy
55
60
  end
56
61
  end
57
62
 
58
- def build_tool_class(tool_name, tool_def, transport)
63
+ def build_tool_class(tool_name, server_uri, tool_def)
59
64
  klass = Class.new(McpTool)
60
65
  klass.instance_variable_set(:@mcp_tool_name, tool_name)
61
- klass.instance_variable_set(:@mcp_transport, transport)
66
+ klass.instance_variable_set(:@mcp_server_uri, server_uri)
62
67
 
63
68
  # Register description and params from the MCP tool definition.
64
69
  klass.description(tool_def[:description] || tool_name)
@@ -66,10 +71,15 @@ module Phronomy
66
71
  klass.param(p[:name].to_sym, type: p[:type]&.to_sym || :string, desc: p[:description].to_s)
67
72
  end
68
73
 
69
- # Define #execute to forward the call to the MCP server.
74
+ # Each instance creates its own transport so concurrent agent threads
75
+ # never share IO streams, eliminating the need for synchronisation.
76
+ klass.define_method(:initialize) do
77
+ uri = self.class.instance_variable_get(:@mcp_server_uri)
78
+ @mcp_transport = self.class.send(:build_transport, uri)
79
+ end
80
+
70
81
  klass.define_method(:execute) do |**args|
71
- self.class.instance_variable_get(:@mcp_transport)
72
- .call_tool(tool_name, args)
82
+ @mcp_transport.call_tool(tool_name, args)
73
83
  end
74
84
 
75
85
  klass
@@ -108,7 +118,6 @@ module Phronomy
108
118
  wait_thr = @wait_thr
109
119
  @stderr_thread = nil
110
120
  @wait_thr = nil
111
- # Join outside the lock to avoid blocking on slow joins.
112
121
  stderr_thread&.join(1)
113
122
  wait_thr&.join(5)
114
123
  end
@@ -208,6 +217,11 @@ module Phronomy
208
217
  @read_timeout = read_timeout
209
218
  end
210
219
 
220
+ # HTTP connections are stateless; close is a no-op, defined so that
221
+ # both transport classes share the same interface as StdioTransport.
222
+ def close
223
+ end
224
+
211
225
  # Retrieve the tool definition from the server using MCP `tools/list`.
212
226
  # @param tool_name [String]
213
227
  # @return [Hash] { description:, parameters: }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/phronomy.rb CHANGED
@@ -5,7 +5,6 @@ require "ruby_llm"
5
5
  require_relative "phronomy/ruby_llm_patches"
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
- loader.ignore(File.expand_path("generators", __dir__))
9
8
  # Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
10
9
  # ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
11
10
  loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
@@ -14,8 +13,6 @@ loader.setup
14
13
  require_relative "phronomy/version"
15
14
  require_relative "phronomy/token_usage"
16
15
 
17
- require "phronomy/railtie" if defined?(Rails::Railtie)
18
-
19
16
  module Phronomy
20
17
  # Exception hierarchy
21
18
  class Error < StandardError; end
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.0
4
+ version: 0.5.1
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-19 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -64,10 +64,6 @@ files:
64
64
  - CHANGELOG.md
65
65
  - README.md
66
66
  - Rakefile
67
- - lib/generators/phronomy/install/install_generator.rb
68
- - lib/generators/phronomy/install/templates/create_phronomy_messages.rb.tt
69
- - lib/generators/phronomy/install/templates/initializer.rb.tt
70
- - lib/generators/phronomy/install/templates/message_model.rb.tt
71
67
  - lib/phronomy.rb
72
68
  - lib/phronomy/agent.rb
73
69
  - lib/phronomy/agent/base.rb
@@ -132,7 +128,6 @@ files:
132
128
  - lib/phronomy/output_parser/json_parser.rb
133
129
  - lib/phronomy/output_parser/structured_parser.rb
134
130
  - lib/phronomy/prompt_template.rb
135
- - lib/phronomy/railtie.rb
136
131
  - lib/phronomy/ruby_llm_patches.rb
137
132
  - lib/phronomy/runnable.rb
138
133
  - lib/phronomy/splitter.rb
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module Phronomy
7
- module Generators
8
- # Rails generator that installs Phronomy into a Rails app.
9
- # Creates an initializer and database migrations:
10
- # - phronomy_messages (conversation history persistence)
11
- #
12
- # Usage:
13
- # rails generate phronomy:install
14
- class InstallGenerator < ::Rails::Generators::Base
15
- include ::Rails::Generators::Migration
16
-
17
- source_root File.expand_path("templates", __dir__)
18
-
19
- desc "Creates a Phronomy initializer and database migrations."
20
-
21
- def self.next_migration_number(dirname)
22
- ::ActiveRecord::Generators::Base.next_migration_number(dirname)
23
- end
24
-
25
- def copy_initializer
26
- template "initializer.rb.tt", "config/initializers/phronomy.rb"
27
- end
28
-
29
- def create_messages_migration
30
- migration_template(
31
- "create_phronomy_messages.rb.tt",
32
- "db/migrate/create_phronomy_messages.rb"
33
- )
34
- end
35
-
36
- def create_message_model
37
- template "message_model.rb.tt", "app/models/phronomy_message.rb"
38
- end
39
- end
40
- end
41
- end
@@ -1,15 +0,0 @@
1
- class CreatePhronomyMessages < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
- def change
3
- create_table :phronomy_messages do |t|
4
- t.string :thread_id, null: false
5
- t.string :role, null: false
6
- t.text :content
7
- t.text :tool_calls_json
8
- t.string :model_id
9
- t.timestamps
10
- end
11
-
12
- add_index :phronomy_messages, :thread_id
13
- add_index :phronomy_messages, [:thread_id, :created_at]
14
- end
15
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Phronomy configuration initializer.
4
- # Customize LLM settings and agent defaults here.
5
- Phronomy.configure do |config|
6
- # Default LLM model used when no model is specified on an agent or chain.
7
- # config.default_model = "gpt-4o-mini"
8
-
9
- # Maximum graph recursion depth (node steps per invoke).
10
- # config.recursion_limit = 25
11
- end
12
-
13
- # RubyLLM provider credentials.
14
- # Prefer Rails credentials (config/credentials.yml.enc) over ENV vars.
15
- RubyLLM.configure do |c|
16
- c.openai_api_key = Rails.application.credentials.dig(:openai, :api_key) || ENV["OPENAI_API_KEY"]
17
- c.anthropic_api_key = Rails.application.credentials.dig(:anthropic, :api_key) || ENV["ANTHROPIC_API_KEY"]
18
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Generated by `rails generate phronomy:install`.
4
- # Stores conversation messages keyed by thread_id.
5
- class PhronomyMessage < ApplicationRecord
6
- include Phronomy::ActiveRecord::ActsAs
7
- acts_as_phronomy_message
8
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Railtie providing Rails integration for Phronomy.
5
- # Loaded only when Rails is present.
6
- class Railtie < ::Rails::Railtie
7
- # Registers generator paths (rails generate phronomy:install).
8
- generators do
9
- require "generators/phronomy/install/install_generator"
10
- end
11
-
12
- # Fills in defaults not already set by a phronomy.rb initializer.
13
- initializer "phronomy.configure_defaults" do
14
- # Passes LLM API keys from Rails credentials to RubyLLM (only when present).
15
- # Use ::Rails to avoid resolving to Phronomy::Rails by accident.
16
- if ::Rails.application.credentials.respond_to?(:openai_api_key) &&
17
- ::Rails.application.credentials.openai_api_key
18
- RubyLLM.configure do |c|
19
- c.openai_api_key = ::Rails.application.credentials.openai_api_key
20
- end
21
- end
22
-
23
- if ::Rails.application.credentials.respond_to?(:anthropic_api_key) &&
24
- ::Rails.application.credentials.anthropic_api_key
25
- RubyLLM.configure do |c|
26
- c.anthropic_api_key = ::Rails.application.credentials.anthropic_api_key
27
- end
28
- end
29
- end
30
-
31
- # Loads Phronomy::Rails::AgentJob when both ActionCable and ActiveJob are present.
32
- initializer "phronomy.agent_job" do
33
- end
34
-
35
- # Loads Phronomy ActiveRecord extensions when ActiveRecord is available.
36
- initializer "phronomy.active_record", after: "active_record.initialize_database" do
37
- end
38
- end
39
- end