phronomy 0.2.2 → 0.3.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -30
  3. data/README.md +26 -110
  4. data/lib/phronomy/agent/base.rb +127 -54
  5. data/lib/phronomy/agent/checkpoint.rb +53 -0
  6. data/lib/phronomy/agent/react_agent.rb +18 -28
  7. data/lib/phronomy/agent/suspend_signal.rb +35 -0
  8. data/lib/phronomy/agent.rb +2 -1
  9. data/lib/phronomy/configuration.rb +0 -24
  10. data/lib/phronomy/guardrail/builtin/pii_pattern_detector.rb +10 -27
  11. data/lib/phronomy/railtie.rb +0 -6
  12. data/lib/phronomy/ruby_llm_patches.rb +20 -0
  13. data/lib/phronomy/tool/mcp_tool.rb +23 -26
  14. data/lib/phronomy/tracing/langfuse_tracer.rb +3 -6
  15. data/lib/phronomy/trust_pipeline.rb +1 -2
  16. data/lib/phronomy/vector_store/redis_search.rb +4 -4
  17. data/lib/phronomy/version.rb +1 -1
  18. data/lib/phronomy/workflow.rb +4 -7
  19. data/lib/phronomy/workflow_runner.rb +1 -8
  20. data/lib/phronomy.rb +1 -0
  21. data/scripts/check_readme_ruby.rb +38 -0
  22. metadata +5 -33
  23. data/docs/trustworthy_ai_enhancements.md +0 -332
  24. data/lib/phronomy/active_record/acts_as.rb +0 -48
  25. data/lib/phronomy/active_record/checkpoint.rb +0 -20
  26. data/lib/phronomy/active_record/extensions.rb +0 -14
  27. data/lib/phronomy/active_record/message.rb +0 -20
  28. data/lib/phronomy/actor.rb +0 -68
  29. data/lib/phronomy/memory/compression/base.rb +0 -37
  30. data/lib/phronomy/memory/compression/summary.rb +0 -107
  31. data/lib/phronomy/memory/compression/tool_output_pruner.rb +0 -67
  32. data/lib/phronomy/memory/compression.rb +0 -11
  33. data/lib/phronomy/memory/conversation_manager.rb +0 -213
  34. data/lib/phronomy/memory/retrieval/base.rb +0 -22
  35. data/lib/phronomy/memory/retrieval/composite.rb +0 -76
  36. data/lib/phronomy/memory/retrieval/recent.rb +0 -35
  37. data/lib/phronomy/memory/retrieval/semantic.rb +0 -114
  38. data/lib/phronomy/memory/retrieval.rb +0 -12
  39. data/lib/phronomy/memory/storage/active_record.rb +0 -248
  40. data/lib/phronomy/memory/storage/base.rb +0 -155
  41. data/lib/phronomy/memory/storage/in_memory.rb +0 -152
  42. data/lib/phronomy/memory/storage.rb +0 -11
  43. data/lib/phronomy/memory.rb +0 -21
  44. data/lib/phronomy/rails/agent_job.rb +0 -75
  45. data/lib/phronomy/state_store/active_record.rb +0 -76
  46. data/lib/phronomy/state_store/base.rb +0 -112
  47. data/lib/phronomy/state_store/encryptor/active_support.rb +0 -49
  48. data/lib/phronomy/state_store/encryptor/base.rb +0 -34
  49. data/lib/phronomy/state_store/encryptor.rb +0 -16
  50. data/lib/phronomy/state_store/file.rb +0 -85
  51. data/lib/phronomy/state_store/in_memory.rb +0 -53
  52. data/lib/phronomy/state_store/redis.rb +0 -70
  53. data/lib/phronomy/state_store.rb +0 -9
  54. data/lib/phronomy/thread_actor_registry.rb +0 -85
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Phronomy
6
- module StateStore
7
- # ActiveRecord-backed state store.
8
- # Persists graph state to a relational database using an AR model.
9
- #
10
- # The model_class must respond to:
11
- # .find_by(thread_id:)
12
- # .find_or_initialize_by(thread_id:)
13
- # #state_json=, #save!
14
- # .where(thread_id:).delete_all
15
- #
16
- # Minimal migration:
17
- # create_table :phronomy_states do |t|
18
- # t.string :thread_id, null: false, index: { unique: true }
19
- # t.text :state_json, null: false
20
- # t.timestamps
21
- # end
22
- #
23
- # @example
24
- # Phronomy.configure do |c|
25
- # c.default_state_store = Phronomy::StateStore::ActiveRecord.new(
26
- # model_class: PhronomyState
27
- # )
28
- # end
29
- class ActiveRecord < Base
30
- # @param model_class [Class] ActiveRecord model with the schema above.
31
- # @param encryptor [Phronomy::StateStore::Encryptor::Base, nil]
32
- # Optional encryption adapter. When supplied, the JSON payload is
33
- # encrypted before writing and decrypted after reading.
34
- # When nil (default), data is stored as plain JSON.
35
- def initialize(model_class:, encryptor: nil)
36
- @model_class = model_class
37
- @encryptor = encryptor
38
- end
39
-
40
- # Serializes and upserts the state for the given thread_id.
41
- # @param state [Object] includes Phronomy::WorkflowContext
42
- # @return [self]
43
- def save(state)
44
- json = serialize_state(state)
45
- payload = @encryptor ? @encryptor.encrypt(json) : json
46
- # Use upsert to avoid a race condition where two concurrent saves for the
47
- # same thread_id would both see "no record" and collide on the unique index.
48
- @model_class.upsert(
49
- {thread_id: state.thread_id, state_json: payload},
50
- unique_by: :thread_id,
51
- update_only: [:state_json]
52
- )
53
- self
54
- end
55
-
56
- # Loads and deserializes the state for the given thread_id.
57
- # @param thread_id [String]
58
- # @return [Object, nil] state instance or nil
59
- def load(thread_id)
60
- record = @model_class.find_by(thread_id: thread_id)
61
- return nil unless record
62
-
63
- payload = record.state_json
64
- json = @encryptor ? @encryptor.decrypt(payload) : payload
65
- deserialize_state(json)
66
- end
67
-
68
- # Deletes the state for the given thread_id.
69
- # @return [self]
70
- def clear(thread_id)
71
- @model_class.where(thread_id: thread_id).delete_all
72
- self
73
- end
74
- end
75
- end
76
- end
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Phronomy
6
- module StateStore
7
- # Abstract base class for state persistence backends.
8
- # Subclasses must implement save, load, and clear.
9
- #
10
- # The state object passed to save must include Phronomy::WorkflowContext
11
- # and have a non-nil thread_id (set automatically by WorkflowRunner#invoke).
12
- class Base
13
- # Persists the state. The thread_id is read from state.thread_id.
14
- # @param state [Object] object including Phronomy::WorkflowContext
15
- # @return [self]
16
- def save(state)
17
- raise NotImplementedError, "#{self.class}#save is not implemented"
18
- end
19
-
20
- # Loads the state for the given thread_id.
21
- # @param thread_id [String]
22
- # @return [Object, nil] state object or nil if not found
23
- def load(thread_id)
24
- raise NotImplementedError, "#{self.class}#load is not implemented"
25
- end
26
-
27
- # Removes the saved state for the given thread_id.
28
- # @param thread_id [String]
29
- # @return [self]
30
- def clear(thread_id)
31
- raise NotImplementedError, "#{self.class}#clear is not implemented"
32
- end
33
-
34
- private
35
-
36
- # Serializes a state object to a JSON string.
37
- # Includes user-defined fields and internal graph metadata.
38
- def serialize_state(state)
39
- JSON.generate(
40
- state_class: state.class.name,
41
- state_data: json_safe(state.to_h),
42
- thread_id: state.thread_id,
43
- phase: state.phase&.to_s
44
- )
45
- end
46
-
47
- # Deserializes a JSON string back into a state object.
48
- # @return [Object] state instance with graph metadata restored
49
- def deserialize_state(json_str)
50
- data = JSON.parse(json_str, symbolize_names: true)
51
- state_class = safe_state_class(data[:state_class])
52
- state_data = symbolize_keys(data[:state_data])
53
- state = state_class.new(**state_data)
54
- state.set_graph_metadata(
55
- thread_id: data[:thread_id],
56
- phase: data[:phase]&.to_sym
57
- )
58
- state
59
- end
60
-
61
- # Resolves and validates a context class name.
62
- # When a registry has been configured via +Phronomy.register_workflow_context+,
63
- # only registered classes are accepted — this prevents unintended autoloading
64
- # of arbitrary files from an untrusted class name stored in Redis/DB.
65
- # When no registry is configured, falls back to Object.const_get with a check
66
- # that the resolved class includes Phronomy::WorkflowContext.
67
- def safe_state_class(class_name)
68
- registry = Phronomy.workflow_context_registry
69
- if registry
70
- klass = registry[class_name.to_s]
71
- unless klass
72
- raise ArgumentError,
73
- "Unregistered context class: #{class_name.inspect}. " \
74
- "Call Phronomy.register_workflow_context(#{class_name}) at startup."
75
- end
76
- return klass
77
- end
78
-
79
- klass = Object.const_get(class_name.to_s)
80
- unless klass.is_a?(Class) && klass.include?(Phronomy::WorkflowContext)
81
- raise ArgumentError, "Invalid context class: #{class_name.inspect}"
82
- end
83
- klass
84
- rescue NameError
85
- raise ArgumentError, "Unknown context class: #{class_name.inspect}"
86
- end
87
-
88
- # Recursively converts objects to JSON-safe primitives.
89
- def json_safe(obj)
90
- case obj
91
- when Hash
92
- obj.transform_keys(&:to_s).transform_values { |v| json_safe(v) }
93
- when Array
94
- obj.map { |v| json_safe(v) }
95
- when String, Numeric, TrueClass, FalseClass, NilClass
96
- obj
97
- else
98
- obj.respond_to?(:to_h) ? json_safe(obj.to_h) : obj.to_s
99
- end
100
- end
101
-
102
- # Recursively symbolizes hash keys.
103
- def symbolize_keys(obj)
104
- case obj
105
- when Hash then obj.transform_keys(&:to_sym).transform_values { |v| symbolize_keys(v) }
106
- when Array then obj.map { |v| symbolize_keys(v) }
107
- else obj
108
- end
109
- end
110
- end
111
- end
112
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module StateStore
5
- module Encryptor
6
- # Encryptor backed by ActiveSupport::MessageEncryptor.
7
- #
8
- # Requires the +activesupport+ gem to be available in the host application.
9
- # Does NOT require rails — any Ruby project that depends on activesupport can
10
- # use this adapter.
11
- #
12
- # @example
13
- # encryptor = Phronomy::StateStore::Encryptor::ActiveSupport.new(
14
- # secret_key_base: ENV.fetch("SECRET_KEY_BASE")
15
- # )
16
- # store = Phronomy::StateStore::ActiveRecord.new(
17
- # model_class: PhronomyState,
18
- # encryptor: encryptor
19
- # )
20
- class ActiveSupport < Base
21
- # @param secret_key_base [String] secret used to derive the encryption key.
22
- # Must be at least 30 random bytes (use +SecureRandom.hex(64)+ to generate).
23
- # @param cipher [String] OpenSSL cipher name (default: "aes-256-gcm").
24
- # @raise [LoadError] when activesupport is not available.
25
- def initialize(secret_key_base:, cipher: "aes-256-gcm")
26
- require "active_support/message_encryptor"
27
- key = ::ActiveSupport::KeyGenerator.new(secret_key_base)
28
- .generate_key("phronomy state store", 32)
29
- @encryptor = ::ActiveSupport::MessageEncryptor.new(key, cipher: cipher)
30
- end
31
-
32
- # Encrypts the plaintext using AES-256-GCM.
33
- # @param plaintext [String]
34
- # @return [String] Base64-encoded authenticated ciphertext
35
- def encrypt(plaintext)
36
- @encryptor.encrypt_and_sign(plaintext)
37
- end
38
-
39
- # Decrypts and verifies the ciphertext.
40
- # @param ciphertext [String]
41
- # @return [String] the original plaintext
42
- # @raise [ActiveSupport::MessageEncryptor::InvalidMessage] on tampered data
43
- def decrypt(ciphertext)
44
- @encryptor.decrypt_and_verify(ciphertext)
45
- end
46
- end
47
- end
48
- end
49
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module StateStore
5
- module Encryptor
6
- # Abstract base class for state encryption adapters.
7
- #
8
- # Subclass and implement {#encrypt} and {#decrypt} to integrate any
9
- # symmetric encryption scheme. Pass an instance to
10
- # {Phronomy::StateStore::ActiveRecord} via the +encryptor:+ argument.
11
- #
12
- # @example
13
- # class MyEncryptor < Phronomy::StateStore::Encryptor::Base
14
- # def encrypt(plaintext) = Base64.strict_encode64(plaintext.reverse)
15
- # def decrypt(ciphertext) = Base64.strict_decode64(ciphertext).reverse
16
- # end
17
- class Base
18
- # Encrypts a plaintext string.
19
- # @param plaintext [String] the JSON string produced by the state store
20
- # @return [String] the encrypted ciphertext
21
- def encrypt(plaintext)
22
- raise NotImplementedError, "#{self.class}#encrypt is not implemented"
23
- end
24
-
25
- # Decrypts a ciphertext string.
26
- # @param ciphertext [String] previously produced by {#encrypt}
27
- # @return [String] the original plaintext
28
- def decrypt(ciphertext)
29
- raise NotImplementedError, "#{self.class}#decrypt is not implemented"
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "encryptor/base"
4
- require_relative "encryptor/active_support"
5
-
6
- module Phronomy
7
- module StateStore
8
- # Namespace for state encryption adapters.
9
- #
10
- # Available adapters:
11
- # - {Phronomy::StateStore::Encryptor::Base} — abstract interface
12
- # - {Phronomy::StateStore::Encryptor::ActiveSupport} — AES-256-GCM via ActiveSupport
13
- module Encryptor
14
- end
15
- end
16
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "json"
5
-
6
- module Phronomy
7
- module StateStore
8
- # File-system-backed state store.
9
- # Persists graph state as a JSON file under a configurable directory.
10
- # No additional server or database migration is required — it works with
11
- # the local file system out of the box.
12
- #
13
- # Each thread_id is stored as a separate file named "<thread_id>.json".
14
- # The thread_id is sanitised before use as a filename to prevent path
15
- # traversal: only alphanumeric characters, hyphens, underscores, and dots
16
- # are allowed; all other characters are replaced with underscores.
17
- #
18
- # @note This store is suitable for single-process use (development, CLI
19
- # tools, tests). It is not safe for concurrent access across multiple
20
- # processes without external locking.
21
- #
22
- # @example
23
- # store = Phronomy::StateStore::File.new(dir: "tmp/workflow_states")
24
- # Phronomy::Workflow.define(MyContext, state_store: store) do
25
- # # ...
26
- # end
27
- class File < Base
28
- # @param dir [String] directory where state files are stored.
29
- # Created automatically if it does not exist.
30
- def initialize(dir: ::File.join(::Dir.tmpdir, "phronomy_states"))
31
- @dir = ::File.expand_path(dir)
32
- ::FileUtils.mkdir_p(@dir)
33
- end
34
-
35
- # @param state [Object] includes Phronomy::WorkflowContext; must have a non-nil thread_id
36
- # @return [self]
37
- def save(state)
38
- ::File.write(path(state.thread_id), serialize_state(state))
39
- self
40
- end
41
-
42
- # @param thread_id [String]
43
- # @return [Object, nil] state instance or nil if not found
44
- def load(thread_id)
45
- file = path(thread_id)
46
- return nil unless ::File.exist?(file)
47
-
48
- deserialize_state(::File.read(file))
49
- end
50
-
51
- # Removes the saved state file for the given thread_id.
52
- # @param thread_id [String]
53
- # @return [self]
54
- def clear(thread_id)
55
- file = path(thread_id)
56
- ::File.delete(file) if ::File.exist?(file)
57
- self
58
- end
59
-
60
- # Removes all state files managed by this store instance.
61
- # @return [self]
62
- def clear_all
63
- ::Dir.glob(::File.join(@dir, "*.json")).each { |f| ::File.delete(f) }
64
- self
65
- end
66
-
67
- # @return [String] the directory used by this store
68
- def directory
69
- @dir
70
- end
71
-
72
- private
73
-
74
- # Converts a thread_id into a safe filename component.
75
- # Characters outside [A-Za-z0-9._-] are replaced with underscores.
76
- def sanitize(thread_id)
77
- thread_id.to_s.gsub(/[^A-Za-z0-9._-]/, "_")
78
- end
79
-
80
- def path(thread_id)
81
- ::File.join(@dir, "#{sanitize(thread_id)}.json")
82
- end
83
- end
84
- end
85
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module StateStore
5
- # In-memory state store backed by per-thread-id {Phronomy::Actor} instances
6
- # from {Phronomy::ThreadActorRegistry}. Suitable for single-process use only.
7
- class InMemory < Base
8
- # Thread-local key for per-thread-id state data (namespaced by store
9
- # instance object_id to support multiple independent InMemory stores).
10
- THREAD_DATA_KEY = :phronomy_state_store_in_memory_data
11
-
12
- def initialize
13
- end
14
-
15
- # @param state [Object] includes Phronomy::WorkflowContext; must have a non-nil thread_id
16
- # @return [self]
17
- def save(state)
18
- store_id = object_id
19
- Phronomy::ThreadActorRegistry.for(state.thread_id).call do
20
- (Thread.current[THREAD_DATA_KEY] ||= {})[store_id] = state
21
- end
22
- self
23
- end
24
-
25
- # @param thread_id [String]
26
- # @return [Object, nil] state object or nil
27
- def load(thread_id)
28
- store_id = object_id
29
- Phronomy::ThreadActorRegistry.for(thread_id).call do
30
- (Thread.current[THREAD_DATA_KEY] ||= {})[store_id]
31
- end
32
- end
33
-
34
- # @param thread_id [String]
35
- # @return [self]
36
- def clear(thread_id)
37
- store_id = object_id
38
- Phronomy::ThreadActorRegistry.for(thread_id).call do
39
- (Thread.current[THREAD_DATA_KEY] ||= {}).delete(store_id)
40
- end
41
- self
42
- end
43
-
44
- def clear_all
45
- store_id = object_id
46
- Phronomy::ThreadActorRegistry.each_actor do |actor|
47
- actor.call { (Thread.current[THREAD_DATA_KEY] ||= {}).delete(store_id) }
48
- end
49
- self
50
- end
51
- end
52
- end
53
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Phronomy
6
- module StateStore
7
- # Redis-backed state store.
8
- # Persists graph state as a JSON string under the key
9
- # "phronomy:state:<thread_id>" in Redis.
10
- #
11
- # The Redis client must be compatible with the redis-rb gem interface:
12
- # client.set(key, value)
13
- # client.get(key)
14
- # client.del(key)
15
- #
16
- # @example
17
- # require "redis"
18
- # redis = Redis.new(url: ENV["REDIS_URL"])
19
- # Phronomy.configure do |c|
20
- # c.default_state_store = Phronomy::StateStore::Redis.new(client: redis)
21
- # end
22
- #
23
- # @example with TTL
24
- # Phronomy::StateStore::Redis.new(client: redis, ttl: 3600)
25
- class Redis < Base
26
- KEY_PREFIX = "phronomy:state:"
27
- private_constant :KEY_PREFIX
28
-
29
- # @param client [#set, #get, #del] Redis-compatible client
30
- # @param ttl [Integer, nil] optional key expiry in seconds
31
- def initialize(client:, ttl: nil)
32
- @client = client
33
- @ttl = ttl
34
- end
35
-
36
- # @param state [Object] includes Phronomy::WorkflowContext
37
- # @return [self]
38
- def save(state)
39
- serialized = serialize_state(state)
40
- if @ttl
41
- @client.set(key(state.thread_id), serialized, ex: @ttl)
42
- else
43
- @client.set(key(state.thread_id), serialized)
44
- end
45
- self
46
- end
47
-
48
- # @param thread_id [String]
49
- # @return [Object, nil] state instance or nil
50
- def load(thread_id)
51
- raw = @client.get(key(thread_id))
52
- return nil unless raw
53
-
54
- deserialize_state(raw)
55
- end
56
-
57
- # @return [self]
58
- def clear(thread_id)
59
- @client.del(key(thread_id))
60
- self
61
- end
62
-
63
- private
64
-
65
- def key(thread_id)
66
- "#{KEY_PREFIX}#{thread_id}"
67
- end
68
- end
69
- end
70
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Namespace for state persistence backends.
5
- # A StateStore saves and loads graph State objects keyed by thread_id.
6
- # The thread_id is embedded in the State itself (state.thread_id).
7
- module StateStore
8
- end
9
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- # Global per-thread-id {Actor} registry.
5
- #
6
- # Maps the +:thread_id+ key from the +config:+ argument passed to
7
- # {Phronomy::Agent::Base#invoke} to a {Phronomy::Actor} instance.
8
- # Each thread_id gets exactly one Actor so that all operations for the same
9
- # conversation are serialised automatically.
10
- #
11
- # @example
12
- # Phronomy::ThreadActorRegistry.for("user-42").call do
13
- # # runs sequentially on the Actor's thread
14
- # end
15
- module ThreadActorRegistry
16
- @actors = {}
17
- @registry_actor = Actor.new
18
-
19
- class << self
20
- # Returns (or lazily creates) the {Actor} for +thread_id+.
21
- #
22
- # When +Phronomy.configuration.max_actors+ is set, the registry evicts the
23
- # least-recently-used Actor (by stopping it) before inserting a new one.
24
- # Accessing an existing Actor moves it to the most-recently-used position.
25
- #
26
- # @param thread_id [String]
27
- # @return [Phronomy::Actor]
28
- def for(thread_id)
29
- @registry_actor.call do
30
- if @actors.key?(thread_id)
31
- # LRU touch: move to end (most-recently used)
32
- actor = @actors.delete(thread_id)
33
- @actors[thread_id] = actor
34
- else
35
- evict_lru_if_needed!
36
- @actors[thread_id] = Actor.new
37
- end
38
- @actors[thread_id]
39
- end
40
- end
41
-
42
- # Returns the current number of registered Actors.
43
- #
44
- # @return [Integer]
45
- def actor_count
46
- @registry_actor.call { @actors.size }
47
- end
48
-
49
- # Gracefully stops the Actor for +thread_id+ and removes it from the
50
- # registry. The next call to {.for} with the same id creates a fresh Actor.
51
- #
52
- # @param thread_id [String]
53
- def stop(thread_id)
54
- @registry_actor.call { @actors.delete(thread_id) }&.stop
55
- end
56
-
57
- # Stops and removes every registered Actor.
58
- # Intended for test teardown and process shutdown.
59
- def clear_all
60
- actors = @registry_actor.call { @actors.values.tap { @actors.clear } }
61
- actors.each(&:stop)
62
- end
63
-
64
- # Yields each currently registered Actor.
65
- # A snapshot is taken so the registry cannot change while callers iterate.
66
- #
67
- # @yield [Phronomy::Actor]
68
- def each_actor(&block)
69
- @registry_actor.call { @actors.values.dup }.each(&block)
70
- end
71
-
72
- private
73
-
74
- # Evicts the least-recently-used Actor when the registry is at capacity.
75
- # Must be called from within @registry_actor.call { } to be thread-safe.
76
- def evict_lru_if_needed!
77
- max = Phronomy.configuration.max_actors
78
- return unless max && @actors.size >= max
79
-
80
- _lru_id, lru_actor = @actors.shift
81
- lru_actor.stop
82
- end
83
- end
84
- end
85
- end